观察者模式(Observer)

老师上完课了,通知学生们说明天要考试

观察者模式

介绍

定义对象之间的一对多依赖,当一个对象的状态改变时,它的所有依赖都会收到通知,并且自动更新状态;

简单理解,允许有一种订阅机制,有个 A 对象,当 A 对象发生一些事情的时候,会调用一个 notify 的方法,来通知其他订阅 A 对象的对象(B、C),然后 B 和 C 做出一些事情;

可以把 A 比作老师,然后 B、C是学生,A 如果说明天考试,那么 B、C 都会收到通知,然后去准备考试;

观察者模式和发布订阅模式的区别:

  • 观察者模式是松耦合的,被观察者是可以直接通知观察者( A 可以直接通知 B );一般用于应用内部
  • 发布订阅模式是完全解耦的,发布者和订阅者之间还有一个 broker 的存在,中间人 broker 转发发布的消息给订阅者;更多的是跨应用的模式,比如常用的消息中间件( kafka )

实现方式

场景

一个用户摇号,然后把摇号结果发送信息给用户,同时也要推送到 MQ 中。

整体设计

代码实现

先定义核心业务类,摇号服务类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MinibusTargetService {

/**
* 模拟摇号
* @param uId 用户id
* @return 结果字符串信息
*/
public String lottery(String uId) {
return Math.abs(uId.hashCode()) % 2 == 0 ?
"恭喜你,编码".concat(uId).concat("在本次摇号中签") :
"很遗憾,编码".concat(uId).concat("在本次摇号未中签或摇号资格已过期");
}
}

下面开始做 UML 中定义的类了,先来定义基本的事件接口:

1
2
3
4
5
6
7
8
public interface EventListener {

/**
* 基本事件
* @param result LotteryResult
*/
void doEvent(LotteryResult result);
}

事件接口实现类:

发送短信通知

1
2
3
4
5
6
7
8
9
10
public class MessageEventListener implements EventListener {

private final Logger logger = LoggerFactory.getLogger(MessageEventListener.class);

@Override
public void doEvent(LotteryResult result) {
// 实际业务逻辑
logger.info("给用户 {} 发送短信通知:{}", result.getuId(), result.getMsg());
}
}

推送 MQ:

1
2
3
4
5
6
7
8
9
public class MQEventListener implements EventListener {

private final Logger logger = LoggerFactory.getLogger(MQEventListener.class);

@Override
public void doEvent(LotteryResult result) {
logger.info("记录用户 {} 摇号结果(MQ)为:{}", result.getuId(), result.getMsg());
}
}

再定义,事件处理类,这个里面维护了 listeners 这个 map 对象,存放 事件类型 和对应的 事件监听者们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class EventManager {

/**
* 存放 事件类型 和对应的 事件监听者们(list)
*/
private final Map<Enum<EventType>, List<EventListener>> listeners = new HashMap<>();

@SafeVarargs
public EventManager(Enum<EventType>... operations) {
for (Enum<EventType> operation : operations) {
listeners.put(operation, new ArrayList<>());
}
}

public enum EventType {
/** MQ类型 */
MQ,
/** 短信息类型 */
Message
}

/**
* 订阅
* @param eventType 事件类型
* @param eventListener 监听
*/
public void subscribe(Enum<EventType> eventType, EventListener eventListener) {
List<EventListener> eventListeners = listeners.get(eventType);
eventListeners.add(eventListener);
}

/**
* 取消监听
* @param eventType 事件类型
* @param eventListener 监听
*/
public void unsubscribe(Enum<EventType> eventType, EventListener eventListener) {
List<EventListener> eventListeners = listeners.get(eventType);
eventListeners.remove(eventListener);
}

/**
* 通知
* @param eventType 事件类型
* @param result 结果
*/
public void notify(Enum<EventType> eventType, LotteryResult result) {
List<EventListener> eventListeners = listeners.get(eventType);
for (EventListener eventListener : eventListeners) {
eventListener.doEvent(result);
}
}


}

摇号业务抽象类,在这个类里面使用 EventManager 进行订阅,通知等操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class LotteryService {

private final EventManager eventManager;

public LotteryService() {
eventManager = new EventManager(EventType.MQ, EventType.Message);
eventManager.subscribe(EventType.MQ, new MQEventListener());
eventManager.subscribe(EventType.Message, new MessageEventListener());
}

public LotteryResult draw(String uId) {
LotteryResult result = doDraw(uId);
// 把结果通知推送到MQ
eventManager.notify(EventType.MQ, result);
// 把结果以短信息形式通知
eventManager.notify(EventType.Message, result);
return result;
}

/**
* 摇号API
* @param uId 用户id
* @return LotteryResult
*/
protected abstract LotteryResult doDraw(String uId);
}

摇号业务实现类,已经是松耦合了,这里面只专注业务方法,不用管繁琐的通知了,在父类已经做了通知:

1
2
3
4
5
6
7
8
9
10
11
12
public class LotteryServiceImpl extends LotteryService {

private final MinibusTargetService minibusTargetService = new MinibusTargetService();

@Override
protected LotteryResult doDraw(String uId) {
// 摇号
String lottery = minibusTargetService.lottery(uId);
// 结果
return new LotteryResult(uId, lottery, new Date());
}
}

测试:

1
2
3
4
5
6
7
@Test
public void testLottery() {
Gson gson = new Gson();
LotteryServiceImpl lotteryService = new LotteryServiceImpl();
LotteryResult result = lotteryService.draw("123123123");
logger.info("测试结果:{}", gson.toJson(result));
}

结果:

1
2
3
23:47:19.376 [main] INFO  i.s.o.event.listener.MQEventListener - 记录用户 123123123 摇号结果(MQ)为:恭喜你,编码123123123在本次摇号中签
23:47:19.380 [main] INFO i.s.o.e.l.MessageEventListener - 给用户 123123123 发送短信通知:恭喜你,编码123123123在本次摇号中签
23:47:19.394 [main] INFO i.sunnyc.observerdemo.test.MainTest - 测试结果:{"uId":"123123123","msg":"恭喜你,编码123123123在本次摇号中签","dateTime":"Mar 12, 2022 11:47:19 PM"}

整体业务时序图

注意已经实现了解耦,业务方法是业务方法那一套,然后业务方法执行完毕后,直接 notify 监听者,他们自己 doEvent即可~