1. 观察者模式介绍
简单来说,当一个行为发生时传递信息给另外一个用户接受做出相应的处理,两者之间没有直接的耦合关联。
在编程开发中也会经常用到一些观察者的模式或组件,如MQ服务,虽然MQ服务是有一个通知中心并不是每一个类服务进行通知,但整体上可以算是观察者模式的思路设计。再比如可能做过的一些类似事件监听总线,让主线服务与其他辅线业务服务分离,为了使系统降低耦合和增强扩展性,也会使用观察者模式思想。
2. 案例场景模拟
在本案例中模拟每次小客车指标摇号事件通知场景。
假如这个类似的摇号功能由你来开发,并且需要对外部的用户做一些事件通知以及需要在主流程外再添加一些额外的辅助流程该如何处理呢?
很多人对这样的通知事件类的实现往往比较粗犷,直接在类里面添加了。但如果仔细思考核心类功能会发现,这里面有一些核心主链路,还有一部分是辅助功能。比如完成某个行为后需要触发MQ给外部,以及做一些消息推送给用户登,这些都不算做核心流程链路,是可以通过事件通知方式进行处理。
场景模拟工程
org.itstack.demo.design
----MinibusTargetService.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("在本次摇号未中签或摇号资格已过期");
}
}
- 非常简单的一个模拟摇号接口,与真实公平的摇号是有区别的。
3. 用一坨坨代码实现
按照需求需要在原有的摇号接口中添加MQ消息发送以及短信通知功能,如果是最直接的方式那么可以直接在方法中补充功能即可。
工程结构
org.itstack.demo.design
----LotteryResult.java
----LotteryService.java
----LotteryServiceImpl.java
- 这里包含三部分内容:返回对象
LotteryResult
、定义接口LotteryService
、具体实现LotteryServiceImpl
。
代码实现
public class LotteryServiceImpl implements LotteryService {
private Logger logger = LoggerFactory.getLogger(LotteryServiceImpl.class);
private MinibusTargetService minibusTargetService = new MinibusTargetService();
public LotteryResult doDraw(String uId) {
// 摇号
String lottery = minibusTargetService.lottery(uId);
// 发短信
logger.info("给用户 {} 发送短信通知(短信):{}", uId, lottery);
// 发MQ信息
logger.info("记录用户 {} 摇号结果(MQ):{}", uId, lottery);
// 结果
return new LotteryResult(uId, lottery, new Date());
}
}
- 可以看到,整体过程包括三部分:摇号、发短信、发MQ消息,而这部分都是顺序调用的。
- 除了摇号接口调用外,后面两部分都是非核心主链路功能,而且随着后续的业务需求发展而不断的调整和扩充,这种开发方式非常不利于维护。
测试验证
编写测试类
@Test
public void test() {
LotteryService lotteryService = new LotteryServiceImpl();
LotteryResult result = lotteryService.doDraw("2765789109876");
logger.info("测试结果:{}", JSON.toJSONString(result));
}
- 测试过程中提供对摇号服务接口的调用。
测试结果
22:02:24.520 [main] INFO o.i.demo.design.LotteryServiceImpl - 给用户
2765789109876 发送短信通知(短信):很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期
22:02:24.523 [main] INFO o.i.demo.design.LotteryServiceImpl - 记录用户
2765789109876 摇号结果(MQ):很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期
22:02:24.606 [main] INFO org.itstack.demo.design.ApiTest - 测试结果:{"dateTime":1598764144524,"msg":"很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期","uId":"2765789109876"}
Process finished with exit code 0
- 从测试结果上是符合预期的,也是平常开发代码的方式,非常简单。
4. 观察者模式重构代码
接下来使用观察者模式来进行代码优化,算是一次很小的重构。
工程结构
org.itstack.demo.design
----event
----listener
----EventListener.java
----MessageEventListener.java
----MQEventListener.java
----EventManager.java
----LotteryResult.java
----LotteryService.java
----LotteryServiceImpl.java
观察者模式模型结构
- 从上图可以分三大块看:事件监听、事件处理、具体的业务流程,另外在业务流程中LotteryService定义的是抽象类,因为这样可以通过抽象类将事件功能屏蔽,外部业务流程开发者不需要知道具体的通知操作。
- 右下角圆圈图表示的是核心流程与非核心流程的结构,一般在开发中会把主线流程开发完成后,再使用通知的方式处理辅助流程。他们可以是异步的,在MQ以及定时任务的处理下,保证最终一致性。
代码实现
事件监听接口定义
public interface EventListener {
void doEvent(LotteryResult result);
}
- 接口中定义了基本的事件类,这里如果方法的入参信息类型是变化的可以使用泛型。
短信事件
public class MessageEventListener implements EventListener {
private Logger logger = LoggerFactory.getLogger(MessageEventListener.class);
@Override
public void doEvent(LotteryResult result) {
logger.info("给用户 {} 发送短信通知(短信):{}", result.getuId(), result.getMsg());
}
}
MQ发送事件
public class MQEventListener implements EventListener {
private Logger logger = LoggerFactory.getLogger(MQEventListener.class);
@Override
public void doEvent(LotteryResult result) {
logger.info("记录用户 {} 摇号结果(MQ):{}", result.getuId(), result.getMsg());
}
}
- 以上是两个事件的具体实现,如果是实际的业务开发会调用外部接口以及控制异常的处理。
- 同时我们上面提到事件接口添加泛型,如果有需要那么在事件的实现中就可以按照不同的类型进行包装事件内容。
事件处理类
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)。
业务抽象类接口
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 = new EventManager(EventManager.EventType.MQ, EventManager.EventType.Message);
- 在使用的时候也是使用枚举的方式进行通知使用,传了什么类型
EventManager.EventType.MQ
,就会执行什么事件通知,按需添加。
业务接口实现类
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());
}
}
- 再看业务流程的实现中已经非常简单了,没有额外的辅助流程,只有核心流程的处理。
测试验证
编写测试类
@Test
public void test() {
LotteryService lotteryService = new LotteryServiceImpl();
LotteryResult result = lotteryService.draw("2765789109876");
logger.info("测试结果:{}", JSON.toJSONString(result));
}
- 从调用上来看几乎没有区别,但是这样的实现方式就可以非常方便的维护代码以及扩展新的需求。
测试结果
23:56:07.597 [main] INFO o.i.d.d.e.listener.MQEventListener - 记录用户
2765789109876 摇号结果(MQ):很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期
23:56:07.600 [main] INFO o.i.d.d.e.l.MessageEventListener - 给用户
2765789109876 发送短信通知(短信):很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期
23:56:07.698 [main] INFO org.itstack.demo.design.test.ApiTest - 测试结果:{"dateTime":1599737367591,"msg":"很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期","uId":"2765789109876"}
Process finished with exit code 0
5. 总结
- 从最基本的过程式开发以及后面使用观察者模式面向对象开发,可以看到设计模式改造后,拆分出了核心流程与辅助流程的代码。一般代码中的核心流程不会经常变化,但辅助流程会随着业务的各种变化而变化,包括:营销、裂变、促活等等,因此使用设计模式显得非常有必要。
- 这种设计模式满足开闭原则,当你需要新增其他监听事件或修改监听逻辑,是不需要改动事件处理类的。但是可能你不能控制调用顺序以及需要做一些事件结果的返回继续操作,所以使用的过程时需要考虑场景的合理性。