观察者模式
# 定义
简单来讲观察者模式,就是当一个行为发生时传递信息给另外一个用户接收做出相应的处理,两者之间没有直接的耦合关联。
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
优点:
1. 观察者和被观察者是抽象耦合的。
2. 建立一套触发机制。
缺点:
1. 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
使用场景:
1. 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
2. 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
3. 一个对象必须通知其他对象,而并不知道这些对象是谁。
4. 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
>注意事项:
>1. JAVA 中已经有了对观察者模式的支持类。
>2. 避免循环引用。
>3. 如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。
>
# 实践
本次我们模拟每次小客车指标摇号事件通知场景
>(真实的不会由官网给你发消息)
>可能大部分人看到这个案例一定会想到自己每次摇号都不中的场景,收到一个遗憾的短信通知。当然目前的摇号系统并不会给你发短信,而是由百度或者一些其他插件发的短信。那么假如这个类似的摇号功能如果由你来开发,并且需要对外部的用户做一些事件通知以及需要在主流程外再添加一些额外的辅助流程时该如何处理呢?
>
>基本很多人对于这样的通知事件类的实现往往比较粗犷,直接在类里面就添加了。1是考虑这可能不会怎么扩展,2是压根就没考虑过。但如果你有仔细思考过你的核心类功能会发现,这里面有一些核心主链路,还有一部分是辅助功能。比如完成了某个行为后需要触发MQ给外部,以及做一些消息PUSH给用户等,这些都不算做是核心流程链路,是可以通过事件通知的方式进行处理。
代码结构:

观察者模式模型结构:

- 从上图可以分为三大块看;事件监听、事件处理、具体的业务流程,另外在业务流程中 LotteryService 定义的是抽象类,因为这样可以通过抽象类将事件功能屏蔽,外部业务流程开发者不需要知道具体的通知操作。
- 右下角圆圈图表示的是核心流程与非核心流程的结构,一般在开发中会把主线流程开发完成后,再使用通知的方式处理辅助流程。他们可以是异步的,在 MQ 以及定时任务的处理下,保证最终一致性。
模拟摇号
```java
public class MinibusTargetService {
/**
* 模拟摇号,但不是摇号算法
*
* @param uId 用户编号
* @return 结果
*/
public String lottery(String uId) {
return Math.abs(uId.hashCode()) % 2 == 0 ? "恭喜你,编码".concat(uId).concat("在本次摇号中签") : "很遗憾,编码".concat(uId).concat("在本次摇号未中签或摇号资格已过期");
}
}
```
事件监听接口定义
```java
public interface EventListener {
void doEvent(LotteryResult result);
}
```
两个监听事件的实现,一个 短消息事件,一个 MQ发送事件
```java
// 短消息事件
@Slf4j
public class MessageEventListener implements EventListener {
@Override
public void doEvent(LotteryResult result) {
log.info("给用户 {} 发送短信通知(短信):{}", result.getUId(), result.getMsg());
}
}
```
```java
// MQ发送事件
@Slf4j
public class MQEventListener implements EventListener {
@Override
public void doEvent(LotteryResult result) {
log.info("记录用户 {} 摇号结果(MQ):{}", result.getUId(), result.getMsg());
}
}
```
- 以上是两个事件的具体实现,相对来说都比较简单。如果是实际的业务开发那么会需要调用外部接口以及控制异常的处理。
- 同时我们上面提到事件接口添加泛型,如果有需要那么在事件的实现中就可以按照不同的类型进行包装事件内容。
事件处理类
```java
public class EventManager {
Map<Enum<EventType>, List<EventListener>> listeners = new HashMap<>();
public EventManager(Enum<EventType>... operations) {
for (Enum<EventType> operation : operations) {
this.listeners.put(operation, new ArrayList<>());
}
}
public enum EventType {
MQ, Message
}
/**
* 订阅
* @param eventType 事件类型
* @param listener 监听
*/
public void subscribe(Enum<EventType> eventType, EventListener listener) {
List<EventListener> users = listeners.get(eventType);
users.add(listener);
}
/**
* 取消订阅
* @param eventType 事件类型
* @param listener 监听
*/
public void unsubscribe(Enum<EventType> eventType, EventListener listener) {
List<EventListener> users = listeners.get(eventType);
users.remove(listener);
}
/**
* 通知
* @param eventType 事件类型
* @param result 结果
*/
public void notify(Enum<EventType> eventType, LotteryResult result) {
List<EventListener> users = listeners.get(eventType);
for (EventListener listener : users) {
listener.doEvent(result);
}
}
}
```
- 整个处理的实现上提供了三个主要方法;订阅(subscribe)、取消订阅(unsubscribe)、通知(notify)。这三个方法分别用于对监听时间的添加和使用。
- 另外因为事件有不同的类型,这里使用了枚举的方式进行处理,也方便让外部在规定下使用事件,而不至于乱传信息(EventType.MQ、EventType.Message)。
业务抽象类接口
```java
public abstract class LotteryService {
private EventManager eventManager;
public LotteryService() {
eventManager = new EventManager(EventManager.EventType.MQ, EventManager.EventType.Message);
eventManager.subscribe(EventManager.EventType.MQ, new MQEventListener());
eventManager.subscribe(EventManager.EventType.Message, new MessageEventListener());
}
public LotteryResult draw(String uId) {
LotteryResult lotteryResult = doDraw(uId);
// 需要什么通知就给调用什么方法
eventManager.notify(EventManager.EventType.MQ, lotteryResult);
eventManager.notify(EventManager.EventType.Message, lotteryResult);
return lotteryResult;
}
protected abstract LotteryResult doDraw(String uId);
}
```
- 这种使用抽象类的方式定义实现方法,可以在方法中扩展需要的额外调用。并提供抽象类 abstract LotteryResult doDraw(String uId),让类的继承者实现。
- 同时方法的定义使用的是 protected,也就是保证将来外部的调用方不会调用到此方法,只有调用到 draw(String uId),才能让我们完成事件通知。
- 此种方式的实现就是在抽象类中写好一个基本的方法,在方法中完成新增逻辑的同时,再增加抽象类的使用。而这个抽象类的定义会有继承者实现。
- 另外在构造函数中提供了对事件的定义;eventManager.subscribe(EventManager.EventType.MQ, new MQEventListener())。
- 在使用的时候也是使用枚举的方式进行通知使用,传了什么类型 EventManager.EventType.MQ,就会执行什么事件通知,按需添加。
业务接口实现类
```java
public class LotteryServiceImpl extends LotteryService {
private MinibusTargetService minibusTargetService = new MinibusTargetService();
@Override
protected LotteryResult doDraw(String uId) {
// 摇号
String lottery = minibusTargetService.lottery(uId);
// 结果
return new LotteryResult(uId, lottery, new Date());
}
}
```
```java
@Data
public class LotteryResult {
// 用户ID
private String uId;
// 摇号信息
private String msg;
// 业务时间
private Date dateTime;
public LotteryResult(String uId, String msg, Date dateTime) {
this.uId = uId;
this.msg = msg;
this.dateTime = dateTime;
}
}
```
测试类
```java
@Slf4j
public class ApiTest {
@Test
public void test() {
LotteryService lotteryService = new LotteryServiceImpl();
LotteryResult result = lotteryService.draw("888");
log.info("测试结果:{}", JSON.toJSONString(result));
}
}
```
测试结果
```java
10:23:43.860 [main] INFO fun.lixj.design.listener.MQEventListener - 记录用户 888 摇号结果(MQ):恭喜你,编码888在本次摇号中签
10:23:43.864 [main] INFO fun.lixj.design.listener.MessageEventListener - 给用户 888 发送短信通知(短信):恭喜你,编码888在本次摇号中签
10:23:43.921 [main] INFO ApiTest - 测试结果:{"dateTime":1645583023860,"msg":"恭喜你,编码888在本次摇号中签","uId":"888"}
Process finished with exit code 0
```
# 总结
- 从我们最基本的过程式开发以及后来使用观察者模式面向对象开发,可以看到设计模式改造后,拆分出了核心流程与辅助流程的代码。一般代码中的核心流程不会经常变化。但辅助流程会随着业务的各种变化而变化,包括;营销、裂变、促活等等,因此使用设计模式架设代码就显得非常有必要。
- 此种设计模式从结构上是满足开闭原则的,当你需要新增其他的监听事件或者修改监听逻辑,是不需要改动事件处理类的。但是可能你不能控制调用顺序以及需要做一些事件结果的返回继续操作,所以使用的过程时需要考虑场景的合理性。
- 任何一种设计模式有时候都不是单独使用的,需要结合其他模式共同建设。另外设计模式的使用是为了让代码更加易于扩展和维护,不能因为添加设计模式而把结构处理更加复杂以及难以维护。这样的合理使用的经验需要大量的实际操作练习而来。
>原文链接:https://github.com/fuzhengwei/CodeGuide/blob/master/docs/md/develop/design-pattern/2020-06-30-%E9%87%8D%E5%AD%A6%20Java%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E3%80%8A%E5%AE%9E%E6%88%98%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md

【设计模式】观察者模式