设计模式之——观察者模式 & CDI实现
前言
观察者模式是常见的设计模式之一,当某个对象行为的改变会引起多个对象的行为也发生改变的场景下,观察者模式就尤为适用。比如说,天气预报说过几天会入冬,那么人们就会对应穿上厚一些的衣服,商城就会进多一些冬季衣服,也就是说信息的接收方会观察数据源并作出相对应的反应。我们常常利用观察者模式来实现代码的简化
本篇文章将围绕观察者模式介绍其核心思想,CDI
对于观察者机制的实现。
一、什么是观察者模式
定义:多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者模式的实现其实特别简单,最少只需要两个对象就行:一个观察者Observer,一个事件发布者(也叫做主题)Subject,后者发生变动时会通知观察者,观察者相对应做出变化。但更加理想化的架构是下图这种,观察者Observer和主题Subject作为顶级接口或者抽象类,分别定义了基本的方法,而具体的方法实现由具体的主题和观察者来定义。
光说概念可能有些抽象,我们不妨用代码来做一个案例的演示:
在狼来了的故事中,每天晚上村民都会随着牧童的动作做出反应,如果牧童说狼来了,那么村民就会拿着武器出来保护羊群,如果牧童没有说狼来了,那么村民就会安心地睡一整晚。
根据要求的描述,我们可以定义出下面的UML
图
我们先定义观察者和通知者两个顶级的接口和抽象类
观察者
public interface Observer {
void response(boolean hasWolf);
}
通知者
public abstract class Notificator {
protected List<Observer> observers = new ArrayList<>();
void addListener(Observer observer){
observers.add(observer);
}
public abstract void notifyObserver(boolean hasWolf);
}
然后分别实现对应的子类:牧童,村民张三和村民李四
牧童
public class ShepherdBoy extends Notificator{
@Override
public void notifyObserver(boolean hasWolf) {
for(Observer villager:observers){
villager.response(hasWolf);
}
}
}
村民张三
public class ZhangSan implements Observer {
@Override
public void response(boolean hasWolf) {
if(hasWolf){
System.out.println("村民张三拿着武器出去了");
}else {
System.out.println("村民张三安眠入睡");
}
}
}
村民李四
public class LiSi implements Observer {
@Override
public void response(boolean hasWolf) {
if(hasWolf){
System.out.println("村民李四拿着武器出去了");
}else{
System.out.println("村民李四安眠入睡");
}
}
}
放入测试类中进行测试:
public class ObserverTest {
public static void main(String[] args) {
ShepherdBoy boy = new ShepherdBoy();
boy.addListener(new ZhangSan());
boy.addListener(new LiSi());
for(int i =0; i<3 ;++i){
System.out.println("第"+(i+1)+"天晚上");
if(i<2){
System.out.println("没有狼");
boy.notifyObserver(false);
System.out.println("============");
}else{
System.out.println("狼来了");
boy.notifyObserver(true);
}
}
}
}
输出结果如下:
可以发现,村民张三和李四会根据不同的情况(狼是否来了)做出反应,这就是简单的观察者模式实现。
二、了解CDI
提供的观察者机制
CDI
(Contexts And Dependency Injection)是JAVA EE
中的一个较为重要的标准规范,主要用于提供容器级别的依赖注入,同时CDI
还提供了Event接口(具体实现由容器厂商完成,文章中使用的依赖是JBOSS
的)来让我们快速实现观察者模式的效果。
想要快速入手CDI
的Event观察者方法,我们需要了解两个重要的知识点:@Observes注解和Event接口
(一)@Observes注解
@Observes
注解是使用在方法的参数上,用于标识该方法属于某一类事件的观察者,将在某一类事件被触发的时候调用。我们可以来看一下这个注解的源码:
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Observes {
Reception notifyObserver() default Reception.ALWAYS;
TransactionPhase during() default TransactionPhase.IN_PROGRESS;
}
可以看到,这个注解中有2个属性,分别对应Reception
和TransactionPhase
两个枚举类。
Reception:该枚举类用于标识触发的观察者方法,分别有IF_EXISTS
和ALWAYS
共2个实例,前者表示只将事件派发给已经初始化的观察者,后者表示会将时间派发给所有应该被通知的观察者(如果尚未初始化,则会进行初始化)。
TransactionPhase
:用于标识该观察者的执行时机,常常与事务配合使用,对应的含义如下表所示:
实例名称 | 实例含义 |
---|---|
IN_PROGRESS | 在事务过程中调用,可以理解为是被包裹在同一个事务中 |
BEFORE_COMPLETION | 观察者方法将在事务完成之前被调用 |
AFTER_COMPLETION | 观察者方法将在事务完成之后被调用 |
AFTER_FAILURE | 事务失败结束后,观察者方法在AFTER_COMPLETION之后被调用 |
AFTER_SUCCESS | 事务成功结束后,观察者方法在AFTER_COMPLETION之后被调用 |
(二)Event接口
Event接口是承载信息的容器,我们想要将某个信息派发给观察者,则必须借助Event容器。接口的源码如下:
public interface Event<T> {
void fire(T var1); // 用于触发观察者
Event<T> select(Annotation... var1);
<U extends T> Event<U> select(Class<U> var1, Annotation... var2);
<U extends T> Event<U> select(TypeLiteral<U> var1, Annotation... var2);
}
我们常常会使用fire
方法来触发观察者方法(具体的实现由容器厂商提供)
(三)小案例
下面我们来通过这两个知识点完成一个小案例,将一个字符串传递到2个以上的观察者上:
步骤一:注入事件Event并调用fire方法
需要注意,我们在定义Event的时候就要顺便定义好传递的信息类型,像当前案例传递的是字符串,所以Event用的泛型就是String
public class CDITestFacade {
@Inject
private Event<String> messageEvent;
public void testCDI(){
testCDIObserver("进行CDI测试...");
}
private void testCDIObserver(String s) {
this.messageEvent.fire(s);
}
}
步骤二:定义多个观察者
需要注意的是,即使是同一个类,也可以有多个针对同一事件的观察者方法(只是这样没什么意义)
观察者一
@ApplicationScoped
public class CDIObserver {
public void listenForCIDTest(@Observes String message){
System.out.println("观察者1接收到的信息1是:"+message);
}
public void listenForCIDTest2(@Observes String message){
System.out.println("观察者1接收到的信息2是:"+message);
}
}
观察者二
@ApplicationScoped
public class CDIObserver2 {
public void listenForCIDTest(@Observes String message){
System.out.println("观察者2接收到的信息是:"+message);
}
}
输出结果:
常见问题总结Q*A
(1)观察者同步方法的执行顺序
如果没有指定优先级,则随机运行(容器在初始化的时候会随机分配一次执行顺序,后面就都会按照这个顺序来执行)。若想要有先后顺序,则通过@Priority
注解进行优先级标识(需要注意的是,这个分配观察者优先级的功能是在CDI2.0
之后才提出来的,要想使用这个特性要注意项目中的版本是否满足)。
(2)怎么样可以快速看到一个事件对应观察者到底有哪些?
很遗憾,似乎并没有一个很便捷的方法来去直接找到事件对应的观察者。容器在初始化观察者时,也是需要通过反射获取事件类型和注解后,才能得到对应的观察者列表。但IDEA提供了一个小插件,可以我们快速查看观察者(有时候会不生效)
(3)@Observes注解下的观察者方法是同步执行还是异步执行的?
答案是同步执行,当Event事件执行fire方法时,会调用依照初始化观察者时分配的顺序进行同步调用,此时线程会被堵塞(即处于Block状态)。如果希望异步执行,则可以使用Event.fireAsync()
方法和@ObservesAsync
配合,这样就可以实现观察者方法的异步调用了。需要注意的是,观察者方法的异步调用是在CDI2.0
之后才提出来的,所以也需要注意项目中的版本是否满足。
(4)观察者是在什么时候开始初始化的?
观察者会在真正被Event事件触发后才开始初始化,但在容器初始化的时候就开始加载其他必要的组件。
三、观察者模式的优势及其劣势
观察者模式的优点
**1)解耦合**。无论是自定义的观察者实现亦或是`CDI`提供的观察者机制,我们都可以看到事件和观察者之间是完全解耦合的,比如说某个商城系统使用了观察者模式,订单生成后只需要将该事件派发出去,至于后续的物流派送,库存更新,用户订单奖励等动作,都由具体的观察者负责。若后续不需要某个观察者方法了,只需要删除对应观察者代码即可,无需更改生产者(即Event事件调用方)的代码。
**2)便于拓展**。定义好一个抽象的公共事件后,即使有不同的场景要求我们传递不同的数据参数,我们也只需要根据实际情况来新建一个主题即可。
观察者模式的缺点
**1)当观察者数量过多时,性能较差。**这个现象主要发生在事件数据量大且观察者方法为同步的场景中,比如卡号合并(这个场景尤为明显),若某个方法中涵盖的事件记录有几百条甚至上千条,而每个事件上面又绑定了10个甚至数量更多的观察者,这样一来**少部分卡号在合并的时候就会调用到数千次甚至上万次观察者方法!!!**这无疑是对性能巨大的损耗。
**2)循环调用下会出现系统奔溃(内存溢出)**,比如说在观察者方法中又进行了`Event.fire()`方法的触发,那就又会触发一轮观察者方法,一直循环调用。这种一般我们写代码的时候注意一下就行。
四、是否有可行的方法来解决观察者模式的缺点呢?
观察者模式主要面临的问题是性能问题,个人觉得解决这个问题主要可以从2个方面入手。一是适当使用异步观察者,若某个场景下观察者方法可以异步执行,比如说有的观察者方法只是单纯的打印日志,我们可以使用异步的方法来处理这部分场景的数据。二是减少观察者(方法)的数量,只唤起必要的观察者,比如商城中使用移动支付购买商品和使用账户积分来兑换商品,都会触发一系列相同的观察者,其中就包括计算奖励积分的观察者方法,但假设使用积分兑换商品根据业务场景不会再奖励一次积分,那么后者就不应该唤起所有的观察者方法。针对这种情况,我们可以采用增加限定符注解的方式来进行限制。