设计模式之——观察者模式 & 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个属性,分别对应ReceptionTransactionPhase两个枚举类。

Reception:该枚举类用于标识触发的观察者方法,分别有IF_EXISTSALWAYS共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个方面入手。一是适当使用异步观察者,若某个场景下观察者方法可以异步执行,比如说有的观察者方法只是单纯的打印日志,我们可以使用异步的方法来处理这部分场景的数据。二是减少观察者(方法)的数量,只唤起必要的观察者,比如商城中使用移动支付购买商品和使用账户积分来兑换商品,都会触发一系列相同的观察者,其中就包括计算奖励积分的观察者方法,但假设使用积分兑换商品根据业务场景不会再奖励一次积分,那么后者就不应该唤起所有的观察者方法。针对这种情况,我们可以采用增加限定符注解的方式来进行限制。

参考文章:

  1. JBOSS官方文档:https://docs.jboss.org/weld/reference/latest/en-US/html/

  2. J2EE官方文档:https://jakarta.ee/specifications/cdi/3.0/jakarta-cdi-spec-3.0.html#introduction

posted @ 2021-11-12 19:29  moutory  阅读(21)  评论(0编辑  收藏  举报  来源