3. 订阅发布模式
概述
订阅发布模式(Publish-Subscribe Pattern)是一种行之有效的解耦框架与业务逻辑的方式,也是一种常见的观察者设计模式,它被广泛应用于事件驱动架构中。
观察者模式的各角色定义如下。
- Subject(目标主题):被观察的目标主题的接口抽象,维护观察者对象列表,并定义注册方法register()(订阅)与通知方法notify()(发布)。对应本章例程中的商店类Shop。
- ConcreteSubject(主题实现):被观察的目标主题的具体实现类,持有一个属性状态State,可以有多种实现。对应本章例程中的商店类Shop。
- Observer(观察者):观察者的接口抽象,定义响应方法update()。对应本章例程中的买家类Buyer。
- ConcreteObserver(观察者实现):观察者的具体实现类,可以有任意多个子类实现。实现了响应方法update(),收到通知后进行自己独特的处理。对应本章例程中的手机买家类PhoneFans、海淘买家类HandChopper
优缺点
优点:
- 解耦:发布者和订阅者不直接依赖,通过消息代理间接通信,实现解耦。
- 扩展性好:发布者和订阅者可以随时增加或删除,消息类别也可以增加,程序的扩展性很好。
- 松散耦合:发布者无需了解所有的订阅者,只管发布消息;订阅者也无需了解所有的发布者,只关注自己感兴趣的消息。
- 增加新的消息与订阅者很方便:消息中心统一管理消息与订阅者的对应关系,增加新消息时,无需修改已有模块。
缺点:
- 复杂性提高:需要引入消息代理和消息分类这两个新的组件,系统结构变得复杂。
- 性能开销:使用消息代理中转消息,会产生额外的性能开销,如网络交互等。
- 依赖中间件:消息代理的可用性会影响系统的可用性,引入了新的依赖点。
- 难以跟踪数据流向:消息在各个组件之间转发,如果系统比较复杂,消息流向会变得不太清晰。
- 不支持同步操作:发布订阅模式以异步消息通知为基础,不适用于同步操作场景。
应用场景
- 构建实时消息系统:比如普通的即时聊天,群聊等功能。发布者可以将消息发送到指定的频道或者主题,订阅者可以根据自己的兴趣或者身份来订阅不同的频道或者主题,并及时收到消息。
- 实现事件驱动的系统:比如前端框架中的事件监听和触发,或者后端框架中的中间件机制。发布者可以将事件作为消息发送出去,订阅者可以根据自己的业务逻辑来订阅不同的事件,并在事件发生时执行相应的操作。
- 实现分布式系统中的消息队列:比如使用 Redis、RabbitMQ、Kafka 等中间件来实现生产者和消费者之间的通信。发布者可以将任务或者数据作为消息发送到队列中,订阅者可以从队列中获取消息并进行处理。
- 实现微信公众号等推送服务:比如用户可以关注不同的公众号或者主题,并在有新内容时收到推送通知。发布者可以将内容作为消息发送到指定的公众号或者主题,订阅者可以根据自己的喜好来订阅不同的公众号或者主题,并在有新内容时收到推送通知。
Java 代码示例
- 创建订阅者接口,用于接受消息通知。
java
复制代码
interface Subscriber { void update(String message); } 2. 创建发布者,用于发布消息。实现了增加、删除和发布的功能,并且维护了一个订阅列表, java 复制代码 class Publisher { private Map<String, List<Subscriber>> subscribers = new HashMap<>(); public void subscribe(String topic, Subscriber subscriber) { List<Subscriber> subscriberList = subscribers.get(topic); if (subscriberList == null) { subscriberList = new ArrayList<>(); subscribers.put(topic, subscriberList); } subscriberList.add(subscriber); } public void unsubscribe(String topic, Subscriber subscriber) { List<Subscriber> subscriberList = subscribers.get(topic); if (subscriberList != null) { subscriberList.remove(subscriber); } } public void publish(String topic, String message) { List<Subscriber> subscriberList = subscribers.get(topic); if (subscriberList != null) { for (Subscriber subscriber : subscriberList) { subscriber.update(message); } } } }
- 我们还实现了两个不同的 Subscriber 实现,一个是 EmailSubscriber,另一个是 SMSSubscriber,用于接受发布者的消息并将其分别发送到邮箱和手机上。
java
复制代码
class EmailSubscriber implements Subscriber { private String email; public EmailSubscriber(String email) { this.email = email; } public void update(String message) { System.out.println("Send email to " + email + ": " + message); } } class SMSSubscriber implements Subscriber { private String phoneNumber; public SMSSubscriber(String phoneNumber) { this.phoneNumber = phoneNumber; } public void update(String message) { System.out.println("Send SMS to " + phoneNumber + ": " + message); }
- 在 Main 类中,我们创建了一个 Publisher 对象,并添加了两个 EmailSubscriber 和两个 SMSSubscriber,分别订阅了 news 主题的更新。我们先给这个主题发送一条消息,然后取消 news 主题的其中一个订阅者,最后我们再次给 news 主题发送一条消息。
java
复制代码
public class Main { public static void main(String[] args) { Publisher publisher = new Publisher(); Subscriber emailSubscriber1 = new EmailSubscriber("foo@example.com"); Subscriber smsSubscriber1 = new SMSSubscriber("1234567890"); publisher.subscribe("news", emailSubscriber1); publisher.subscribe("news", smsSubscriber1); publisher.publish("news", "发布新消息1"); publisher.unsubscribe("news", smsSubscriber1); publisher.publish("news", "发布新消息2"); } }
打印输出如下:
scss
复制代码
Send email to foo@example.com: 发布新消息1 Send SMS to 1234567890: 发布新消息1 Send email to foo@example.com: 发布新消息2
Spring 代码示例
Spring的订阅发布模式是通过发布事件、事件监听器和事件发布器3个部分来完成的
这里我们通过 newbee-mall-pro 项目中已经实现订阅发布模式的下单流程给大家讲解,项目地址:github.com/wayn111/new…
- 自定义订单发布事件,继承 ApplicationEvent
java
复制代码
public class OrderEvent extends ApplicationEvent { void onApplicationEvent(Object event) { ... } }
单监听器,实现 ApplicationListener
java
复制代码
@Component public class OrderListener implements ApplicationListener<OrderEvent> { @Override public void onApplicationEvent(OrderEvent event) { // 生成订单、删除购物车、扣减库存 ... } }
- 下单流程,通过事件发布器 applicationEventPublisher 发布订单事件,然后再订单监听器中处理订单保存逻辑。
java
复制代码
@Resource private ApplicationEventPublisher applicationEventPublisher; private void saveOrder(MallUserVO mallUserVO, Long couponUserId, List<ShopCatVO> shopcatVOList, String orderNo) { // 订单检查 ... // 生成订单号 String orderNo = NumberUtil.genOrderNo(); // 发布订单事件,在事件监听中处理下单逻辑 applicationEventPublisher.publishEvent(new OrderEvent(orderNo, mallUserVO, couponUserId, shopcatVOList)); // 所有操作成功后,将订单号返回 return orderNo; ... }
通过事件监听机制,我们将下单逻辑拆分成如下步骤:
- 订单检查
- 生成订单号
- 发布订单事件,在事件监听中处理订单保存逻辑
- 所有操作成功后,将订单号返回
每个步骤都是各自独立
如上的代码已经实现了订阅发布模式,成功解耦了下单逻辑。建议大家在日常开发中多加思考哪些业务流程可以适用,例如微服务项目中订单支付成功后需要通知用户、商品、活动等多个服务时,可以考虑使用订阅发布模式。解耦发布者和订阅者,发布者只管发布消息,不需要知道有哪些订阅者,也不需要知道订阅者的具体实现。订阅者只需要关注自己感兴趣的消息即可。这种松耦合的设计使得系统更容易扩展和维护。
4. 策略模式
概述
策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一组同类型的算法,在不同的类中封装起来,每种算法可以根据当前场景相互替换,从而使算法的变化独立于使用它们的客户端(即算法的调用者)。
策略模式的各角色定义如下。
- Strategy(策略接口):定义通用的策略规范标准,包含在系统环境中并声明策略接口标准。对应本章例程中的USB接口USB。
- ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC……(策略实现):实现了策略接口的策略实现类,可以有多种不同的策略实现,但都得符合策略接口定义的规范。对应本章例程中的USB键盘类Keyboard、USB鼠标类Mouse、USB摄像头类Camera。
- Context(上下文):包含策略接口的系统环境,对外提供更换策略实现的方法setStrategy()以及执行策略的方法executeStrategy(),其本身并不关心执行的是哪种策略实现。对应本章例程中的计算机主机类Computer。
优缺点
优点:
- 可以避免使用多重条件语句,提高代码的可读性和可维护性。
- 可以提供多种可重用的算法族,减少代码的重复。
- 可以实现开闭原则,增加新的算法或者修改旧的算法不影响原有的结构。
- 可以灵活地切换不同的算法,增加系统的灵活性。
缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类,这增加了客户端的复杂度。
- 策略模式会产生很多的策略类,增加系统的类数量。
应用场景
策略模式适用于以下场景:
- 一个系统有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种。
- 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
Java代码示例
假设我们有一个计算器程序,它可以根据用户输入的不同运算符(+、-、*、/)来执行不同的算术运算。我们可以使用策略模式来实现这个功能,具体步骤如下:
1. 定义一个策略接口
我们首先定义一个策略接口 Strategy ,它声明了一个 doOperation 方法,用于执行具体的运算。
java
复制代码
// 策略接口 public interface Strategy { // 执行运算 public int doOperation(int num1, int num2); }
2. 实现具体的策略类
然后我们实现四个具体的策略类,分别是 AddStrategy 、SubtractStrategy 、MultiplyStrategy 和 DivideStrategy ,它们都实现了 Strategy 接口,并重写了 doOperation 方法。
java
复制代码
// 加法策略 public class AddStrategy implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 + num2; } }
// 减法策略 public class SubtractStrategy implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 - num2; } } // 乘法策略 public class MultiplyStrategy implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 * num2; } } // 除法策略 public class DivideStrategy implements Strategy { @Override public int doOperation(int num1, int num2) { if (num2 == 0) { throw new IllegalArgumentException("除数不能为0"); } return num1 / num2; } }
3. 定义一个上下文类
接下来我们定义一个上下文类 Context ,它持有一个 Strategy 的引用,并提供了一个构造方法和一个 executeStrategy 方法。构造方法用于传入具体的策略对象,executeStrategy 方法用于调用策略对象的 doOperation 方法。
java
复制代码
// 上下文类 public class Context { private Strategy strategy; public Context(Strategy strategy) { this.strategy = strategy; } public int executeStrategy(int num1, int num2) { return strategy.doOperation(num1, num2); } }
4. 测试策略模式
最后我们编写一个测试类,用于创建不同的策略对象和上下文对象,并根据用户的输入来执行不同的算法。
java
复制代码
// 测试类 public class StrategyTest { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入第一个数:"); int num1 = scanner.nextInt(); System.out.println("请输入运算符(+、-、*、/):"); String operator = scanner.next(); System.out.println("请输入第二个数:"); int num2 = scanner.nextInt(); scanner.close(); // 根据运算符创建不同的策略对象 Strategy strategy; switch (operator) { case "+" -> strategy = new AddStrategy(); case "-" -> strategy = new SubtractStrategy(); case "*" -> strategy = new MultiplyStrategy(); case "/" -> strategy = new DivideStrategy(); default -> { System.out.println("无效的运算符"); return; } } // 创建上下文对象,并传入策略对象 Context context = new Context(strategy); // 调用上下文对象的方法,执行策略对象的算法 int result = context.executeStrategy(num1, num2); // 输出结果 System.out.println(num1 + " " + operator + " " + num2 + " = " + result); } }
运行结果:
ini
复制代码
请输入第一个数: 1 请输入运算符(+、-、*、/): + 请输入第二个数: 1 1 + 1 = 2
Spring 代码示例
在 Spring 框架中,也有很多地方使用了策略模式,比如 BeanFactory 的实现类,它们都实现了一个 BeanFactory 接口,但是具体的实例化和管理 Bean 的方式不同。比如 XmlBeanFactory 是从 XML 文件中读取 Bean 的定义,而 AnnotationConfigApplicationContext 是从注解中读取 Bean 的定义。
我们可以使用 Spring 的依赖注入功能,来实现策略模式,具体步骤如下:
1. 定义一个策略接口
我们还是使用上面的计算器程序作为例子,首先定义一个策略接口 Strategy ,它声明了一个 doOperation 方法,用于执行具体的运算。
java
复制代码
// 策略接口 public interface Strategy { // 执行运算 public int doOperation(int num1, int num2); }
2. 实现具体的策略类
然后我们实现四个具体的策略类,分别是 AddStrategy 、SubtractStrategy 、MultiplyStrategy 和 DivideStrategy ,它们都实现了 Strategy 接口,并重写了 doOperation 方法。同时,我们给每个策略类添加一个 @Component 注解,表示它们是 Spring 容器管理的组件。
java
复制代码
// 加法策略 @Component public class AddStrategy implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 + num2; } } // 减法策略 @Component public class SubtractStrategy implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 - num2; } } // 乘法策略 @Component public class MultiplyStrategy implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 * num2; } } // 除法策略 @Component public class DivideStrategy implements Strategy { @Override public int doOperation(int num1, int num2) { if (num2 == 0) { throw new IllegalArgumentException("除数不能为0"); } return num1; } }
好的,我继续写。
3. 定义一个上下文类
接下来我们定义一个上下文类 Context ,它持有一个 Strategy 的引用,并提供了一个构造方法和一个 executeStrategy 方法。构造方法用于传入具体的策略对象,executeStrategy 方法用于调用策略对象的 doOperation 方法。同时,我们给上下文类添加一个 @Component 注解,表示它也是 Spring 容器管理的组件。
java
复制代码
// 上下文类 @Component public class Context { private Strategy strategy; public Context(Strategy strategy) { this.strategy = strategy; } public int executeStrategy(int num1, int num2) { return strategy.doOperation(num1, num2); } }
4. 使用 @Autowired 注入策略对象
为了让 Spring 容器能够自动注入不同的策略对象,我们需要使用 @Autowired 注解来标注 Context 类的构造方法,并使用 @Qualifier 注解来指定具体的策略类。这样,我们就可以根据不同的运算符来创建不同的上下文对象,而不需要手动创建策略对象。
java
复制代码
// 上下文类 @Component public class Context { @Autowired private List<Strategy> list; public Strategy getBean(Class tClass) throws Exception { for (Strategy strategy : list) { if (strategy.getClass() == tClass) { return strategy; } } throw new Exception("获取策略失败"); } }
5. 测试策略模式
最后我们编写一个测试类,用于从 Spring 容器中获取 Context 对象,并根据用户的输入来执行不同的算法。
java
复制代码
// 测试类 @SpringBootTest @RunWith(SpringRunner.class) public class StrategyTest { // 从Spring容器中获取Context对象 @Autowired private Context context; @Test public void test() throws Exception { System.out.println("请输入第一个数:2"); int num1 = 2; System.out.println("请输入运算符(+、-、*、/):*"); String operator = "*"; System.out.println("请输入第二个数:3"); int num2 = 3; // 根据运算符创建不同的上下文对象 Strategy strategy; switch (operator) { case "+" -> strategy = context.getBean(AddStrategy.class); case "-" -> strategy = context.getBean(SubtractStrategy.class); case "*" -> strategy = context.getBean(MultiplyStrategy.class); case "/" -> strategy = context.getBean(DivideStrategy.class); default -> { System.out.println("无效的运算符"); return; } } // 调用上下文对象的方法,执行策略对象的算法 int result = strategy.doOperation(num1, num2); // 输出结果 System.out.println(num1 + " " + operator + " " + num2 + " = " + result); } }
运行结果:
ini
复制代码
请输入第一个数:2 请输入运算符(+、-、*、/):* 请输入第二个数:3 2 * 3 = 6
总的来说策略模式是一种常用的行为型设计模式,它可以将不同的算法封装在不同的类中,并让它们可以相互替换。策略模式可以避免使用多重条件语句,提高代码的可读性和可维护性,同时也可以实现开闭原则,增加系统的灵活性。但是策略模式也会增加客户端和系统的复杂度,因此需要根据具体的情况来权衡利弊。
总结
至此本文所讲的四种常用设计模式就全部介绍完了,希望能对大家有所帮助。