观察者模式介绍

简单来讲,观察者模式是指当一个行为发生时,一个用户传递信息,另一个用户接受信息并做出相应的处理,行为和接受者之间没有直接的耦合关联。

在编程开发中,也会常用到一些观察者模式或组件,例如经常使用的MQ服务。虽然MQ服务有一个通知中心,但服务并不会通知每一个类。再比如时间监听总线,主线服务与其他辅线业务服务分离,为了降低系统耦合和增强性扩展性,也会使用观察者模式。

模拟场景

车辆摇号通知场景

可能大部分人看到这个案例一定会想到自己每次摇号都不中的场景,收到一个遗憾的短信通知。当然目前的摇号系统并不会给你发短信,而是由百度或者一些其他插件发的短信。那么假如这个类似的摇号功能如果由你来开发,并且需要对外部的用户做一些事件通知以及需要在主流程外再添加一些额外的辅助流程时该如何处理呢?

基本很多人对于这样的通知事件类的实现往往比较粗犷,直接在类里面就添加了。1是考虑这可能不会怎么扩展,2是压根就没考虑过。但如果你有仔细思考过你的核心类功能会发现,这里面有一些核心主链路,还有一部分是辅助功能。比如完成了某个行为后需要触发MQ给外部,以及做一些消息PUSH给用户等,这些都不算做是核心流程链路,是可以通过事件通知的方式进行处理。

那么接下来我们就使用这样的设计模式来优化重构此场景下的代码。

摇号服务接口

1
2
3
4
5
6
7
public class MinibusTargetService {
public String lottery(String uId){
return Math.abs(uId.hashCode()) % 2 == 0 ? "恭喜你,编码".concat(uId).concat("在本次摇号中中签")
:"很遗憾,编码:".concat(uId).concat("在本次摇号中未中签或签好资格已过期");
}
}

纯粹的模拟一下

违背设计模式实现

按照需求,需要在原有的摇号中添加MQ消息,并提供发送功能及短信通知功能,最直接的方式是直接在方法中补充

返回对象(LotteryResult),结果类

1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LotteryResult {
private String uId;
private String msg;
private Date dateTime;
}

定义接口(LotteryService),主要是定义摇号接口

1
2
3
public interface LotteryService {
LotteryResult doDraw(String uId);
}

具体实现(LotteryServiceImpl),逻辑的实现,给用户发送通知短信

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LotteryServiceImpl implements LotteryService{
private MinibusTargetService minibusTargetService = new MinibusTargetService();
@Override
public LotteryResult doDraw(String uId) {
//摇号
String lottery = minibusTargetService.lottery(uId);
//发送通知短信
System.out.println("给用户: "+uId+"发送短信"+lottery);
//发送MQ消息
return new LotteryResult(uId,lottery,new Date());

}
}

从以上的方法实现中可以看到,整体过程包括三个部分:摇号、发送通知、发送MQ消息,三者都是顺序调用的。除了摇号调用接口,后面的两部分都是非核心主链路功能,而会随着后续的业务需求发展不断地调整和扩充,在这种开发方式下将非常不利于后期和维护。

观察者模式重构代码

下面使用观察者模式,将代码按照职责流程拆分,把混合到一起的摇号和发送通知分别放到业务核心流程和监听事件中实现,通过这种实现方式,可以让核心流程的代码简单干净且易于扩展,而监听事件可以做响应的业务扩展,不影响主流程。

image-20211207174635802

可以分为三大块:监听事件、事件处理和具体的业务流程。另外,在业务流程中,LotteryService定义的是抽象类,因为这样可以通过抽象类将事件功能屏蔽,外部业务流程开发者不需要知道具体的通知操作。

监听事件接口定义

1
2
3
4
import com.bestrookie.design.LotteryResult;
public interface EventListener {
void doEvent(LotteryResult result);
}

接口中定义了基本的时间类,如果方法的入参信息类型是变化的,则可以使用泛型

短信消息事件

1
2
3
4
5
6
7
import com.bestrookie.design.LotteryResult;
public class MessageEventListener implements EventListener{
@Override
public void doEvent(LotteryResult result) {
System.out.println("Message: 给用户"+result.getUId()+"发送消息: "+result.getMsg());
}
}

MQ发送事件

1
2
3
4
5
6
7
import com.bestrookie.design.LotteryResult;
public class MQEventListener implements EventListener{
@Override
public void doEvent(LotteryResult result) {
System.out.println("MQ: 记录用户:"+result.getUId()+" 摇号结果: "+result.getMsg());
}
}

以上是两个事件的具体实现,相对来说都比较简单。如果是实际业务开发,则会需要调用外部接口以及控制异常的处理。同时,上面提到事件接口添加泛型。如果有添加的需要,那么在事件的实现中可以按照不同的类型包装事件内容。

事件处理类

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
import com.bestrookie.design.LotteryResult;
import com.bestrookie.design.event.listener.EventListener;
import com.oracle.javafx.jmx.json.JSONReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class EventManager {
Map<Enum<EventType>, List<EventListener>> listeners = new HashMap<>();
@SafeVarargs
public EventManager(Enum<EventType>... operations){
for (Enum<EventType> operation : operations) {
this.listeners.put(operation,new ArrayList<>());
}
}

/**
* 订阅
* @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);
}

public void notify(Enum<EventType> eventType, LotteryResult result){
List<EventListener> users = listeners.get(eventType);
for (EventListener user : users){
user.doEvent(result);
}

}
}

枚举类

1
2
3
public enum EventType {
MQ,Message
}

在处理的实现方面提供了三种主要方式:订阅(subscribe)、取消定于(unsubscribe)、和通知(notify),分别用于监听事件的添加和使用。因为事件有不同的类型,这里使用了枚举类(EventType.MQ、EventType.Message)的规定下使用事件服务,而不至于错误传递调用信息。

业务抽象类接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.bestrookie.design.event.EventManager;
import com.bestrookie.design.event.EventType;
import com.bestrookie.design.event.listener.MQEventListener;
import com.bestrookie.design.event.listener.MessageEventListener;
public abstract class LotteryService {
private 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 id){
LotteryResult lotteryResult = doDraw(id);
eventManager.notify(EventType.MQ,lotteryResult);
eventManager.notify(EventType.Message,lotteryResult);
return lotteryResult;
}
protected abstract LotteryResult doDraw(String uId);
}

使用抽象类的方式定义实现方法,可以在方法中扩展需要的额外调用,并提供抽象类abstract LotteryResult doDraw(String uId),让类的继承者实现。同时,方法的定义使用的是protected,也就是保证将来外部的调用方不会调用到此方法,只有调用到draw(String uId)才能完成事件通知。此种方式的实现是在抽象类中写好一个基本的方法,在方法中完成新增逻辑的同时,在增加抽象类的使用,而这个抽象类的定义会由继承者实现。另外,在构造函数中提供了对事件的定义:eventManager.subscribe(EventType.Mq,new MQEvenetListener())。在使用时也采用枚举的方式通知使用者,传了哪些类型,就执行哪些事件通知,按需添加。

业务接口实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.bestrookie.MinibusTargetService;
import java.util.Date;
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());
}
}

对于业务流程的实现,可以看到已经非常的简单了,没有额外的辅助流程,只有核心流程的处理。

总结

从基本的过程式开发,到使用观察者模式面向对象开发,可以看到使用设计模式改造后,拆分出了核心流程与辅助流程的代码。代码中的核心流程一般不会经常变化,辅助流程会随着业务的变化而变化,包括影响、裂变和促活等,因此使用设计模式编码就显得非常有必要。

此种设计模式从结构上满足开闭原则,当需要新增其他的监听事件或修改监听逻辑时,不需要改动事件处理类。但可能不能控制调用顺序以及需要做一些事情结果的返回操作。所以在使用的过程时需要考虑场景的适用性。任何一种设计模式有时都不是单独使用的,需要结合其他模式共同使用。

另外,设计模式的使用是为了让代码更加易于扩展和维护,不能因为添加设计模式而把结构处理得更加复杂甚至难以维护。要想合理地使用设计模式,需要大量的实际操作经验。

傅哥yyds

评论