设计模式之观察者模式

语言: CN / TW / HK

观察者模式是极其重要的一个设计模式,也是我几年开发过程中使用最多的设计模式,本文首先概述观察者模式的基本概念和Demo实现,接着是观察者模式在Java和Spring中的应用,最后是对观察者模式的应用场景和优缺点进行总结。

一、概念理解

观察者模式:定义对象之间的一种一对多的依赖关系,使得每当一个对象的状态发生变化时,其相关的依赖对象都可以得到通知并被自动更新。主要用于多个不同的对象对一个对象的某个方法会做出不同的反应!

概念啥意思呢?也就是说,如果使用观察者模式在A的业务逻辑中调用B的业务逻辑,即使B的业务逻辑报错了,仍然不影响A的执行。

比如,在我最近公司开发商城系统的过程中,提交订单成功以后要删除购物车中的信息,如果我先写订单提交逻辑,接着写删除购物车逻辑,这样当然没有什么问题,但是这样程序的健壮性太差了。

应该将该业务分成两步,一是处理订单成功处理逻辑,二是删除购物车中的信息。即使删除购物车报错了,提交订单逻辑仍然不影响。

那应该怎么做才能让他们互不影响呢?需要在购物车对象中要有一个方法用于删除购物车,还要有一个对象A用于注入(add)购物车对象和通知(notify)购物车执行它的方法。

在执行时先调用对象A的add方法将购物车对象添加到对象A中,在订单提交成功以后,调用对象A的通知notify购物车方法执行清除购物车逻辑。

在观察者模式中,购物车就称为观察者,对象A就称为目标对象。在面向接口编程原则下,观察者模式应该包括四个角色:

1、目标接口(subject) :它是一个抽象类,也是所有目标对象的父类。它用一个列表记录当前目标对象有哪些观察者对象,并提供增加、删除观察者对象和通知观察者对象的方法声明。

2、具体目标类:可以有多个不同的具体目标类,它们同时继承Subject类。一个目标对象就是某个具体目标类的对象,一个具体目标类负责定义它自身的事务逻辑,并在状态改变时通知它的所有观察者对象。

3、观察者接口(Listener) 它也是一个抽象类,是所有观察者对象的父类;它为所有的观察者对象都定义了一个名为update(notify)的方法。当目标对象的状态改变时,它就是通过调用它的所有观察者对象的update(notify)方法来通知它们的。

4、具体观察者类,可以有多个不同的具体观察者类,它们同时继承Listener类。一个观察者对象就是某个具体观察者类的对象。每个具体观察者类都要重定义Listener类中定义的update(notify)方法,在该方法中实现它自己的任务逻辑,当它被通知的时候(目标对象调用它的update(notify)方法)就执行自己特有的任务。在我们的例子中是购物车观察者,当然还能有别的,如日志观察者。

我们基于四个角色实现demo。

二、案例实现

目标接口:包括注册、移除、通知监听者的方法声明。

/**
 * 这是被观察的对象
 * 目标类
 * @author tcy
 * @Date 17-09-2022
 */
public interface SubjectAbstract<T> {
    //注册监听者
    public void registerListener(T t);
    //移除监听者
    public void removeListener(T t);
    //通知监听者
    public void notifyListener();
}

目标接口实现:里面需要一个listenerList数组存储所有的观察者,需要定义add和remove观察者的方法,需要给出notify方法通知所有的观察者对象。

/**
 * 
 * 具体目标类
 * @author tcy
 * @Date 17-09-2022
 */
public class SubjectImpl implements SubjectAbstract<ListenerAbstract> {

    //监听者的注册列表
    private List<ListenerAbstract> listenerList = new ArrayList<>();

    @Override
    public void registerListener(ListenerAbstract myListener) {
        listenerList.add(myListener);
    }

    @Override
    public void removeListener(ListenerAbstract myListener) {
        listenerList.remove(myListener);
    }

    @Override
    public void notifyListener() {
        for (ListenerAbstract myListener : listenerList) {
            System.out.println("收到推送事件,开始调用异步逻辑...");
            myListener.onEvent();
        }
    }
}

观察者接口:声明响应方法

/**
 * 
 * 观察者-接口
 * @author tcy
 * @Date 17-09-2022
 */
public interface ListenerAbstract {
    void onEvent();
}

观察者接口:实现响应方法,处理清除购物车的逻辑。

/**
 * 具体观察者类 购物车
 * @author tcy
 * @Date 17-09-2022
 */
public class ListenerMyShopCart implements ListenerAbstract {
    @Override
    public void onEvent() {

            //...省略购物车处理逻辑
            System.out.println("删除购物车中的信息...");

    }
}

我们使用Client模拟提交订单操作。

/**
 * 先使用具体目标对象的registerListener方法添加具体观察者对象,
 * 然后调用其notify方法通知观察者
 * @author tcy
 * @Date 17-09-2022
 */
public class Client {
    public static void main(String[] args) {

        System.out.println("订单成功处理逻辑...");
        //创建目标对象
        SubjectImpl subject=new SubjectImpl();

        //具体观察者注册入 目标对象
        ListenerMyShopCart shopCart=new ListenerMyShopCart();
        //向观察者中注册listener
        subject.registerListener(shopCart);

        //发布事件,通知观察者
        subject.notifyListener();

    }
}

这样就实现了订单的处理逻辑和购物车的逻辑解耦,即使购物车逻辑报错也不会影响订单处理逻辑。

既然观察者模式是很常用的模式,而且抽象观察者和抽象目标类方法声明都是固定的,作为高级语言Java,Java设计者干脆内置两个接口,开发者直接实现接口就能使用观察者模式。

三、Java中的观察者模式

在 Java 中,通过 java.util.Observable 类和 java.util.Observer 接口定义观察者模式,只要实现它们的子类就可以编写观察者模式实例。

Observable 类是抽象目标类,它有一个 Vector 向量,用于保存所有要通知的观察者对象,下面来介绍它最重要的 3 个方法。

void addObserver(Observer o) 方法:用于将新的观察者对象添加到向量中。

void notifyObservers(Object arg) 方法:调用向量中的所有观察者对象的 update() 方法,通知它们数据发生改变。通常越晚加入向量的观察者越先得到通知。

void setChange() 方法:用来设置一个 boolean 类型的内部标志位,注明目标对象发生了变化。当它为真时,notifyObservers() 才会通知观察者。

Observer 接口是抽象观察者,它监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 void update(Observable o,Object arg) 方法,进行相应的工作。

我们基于Java的两个接口,改造我们的案例。

具体目标类:

/**
 * 具体目标类 
 * @author tcy
 * @Date 19-09-2022
 */
public class SubjectObservable extends Observable {

    public void notifyListener() {
        super.setChanged();
        System.out.println("收到推送的消息...");
        super.notifyObservers();    //通知观察者购物车事件
    }

}

具体观察者类:

/**
 * 观察者实现类
 * @author tcy
 * @Date 19-09-2022
 */
public class ShopCartObserver implements Observer {

    @Override
    public void update(Observable o, Object arg) {
        System.out.println("清除购物车...");

    }
}

依旧是Client模拟订单处理逻辑。

/**
 * @author tcy
 * @Date 19-09-2022
 */
public class Client {
    public static void main(String[] args) {
        System.out.println("订单提交成功...");
        SubjectObservable observable = new SubjectObservable();

        Observer shopCartObserver = new ShopCartObserver(); //购物车

        observable.addObserver(shopCartObserver);
        observable.notifyListener();


    }

}

这样也能实现观察者逻辑,但Java中的观察者模式有一定的局限性。

Observable是个类,而不是一个接口,没有实现Serializable,所以,不能序列化和它的子类,而且他是线程不安全的,无法保证观察者的执行顺序。在JDK9之后已经启用了。

写Java的恐怕没有不用Spring的了,作为优秀的开源框架,Spring中也有观察者模式的大量应用,而且Spring是在java的基础之上改造的,很好的规避了Java观察者模式的不足之处。

四、Spring如何使用观察者模式

在第一章节典型的观察者模式中包含着四个角色:目标类、目标类实现、观察者、观察者实现类。而在Spring下的观察者模式略有不同,Spring对其做了部分改造。

事件:

Spring中定义最顶层的事件ApplicationEvent,这个接口最终还是继承了EventObject接口。

只是在基础之上增加了构造和获取当前时间戳的方法,Spring所有的事件都要实现这个接口,比如Spring中内置的ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent…看名字大概就知道这些事件用于哪些地方,分别是容器刷新后、开始时、停止时…

目标类接口:

Spirng中的ApplicationEventMulticaster接口就是实例中目标类,我们可以对比我们的目标接口和ApplicationEventMulticaster接口,长的非常像。

观察者接口:

观察者ApplicationListener用于监听事件,只有一个方法onApplicationEvent事件发生后该事件执行。与我们样例中的抽象观察者并无太大的不同。

目标类实现:

在我们案例中目标类的职责直接在一个类中实现,注册监听器、广播事件(调用监听器方法)。

在Spring中两个实现类分别拆分开来,Spring启动过程中会调用registerListeners()方法,看名字我们大概就已经知道是注册所有的监听器,该方法完成原目标类的注册监听器职责。

在Spring中事件源 ApplicationContext 用于广播事件,用户不必再显示的调用监听器的方法,交给Spring调用,该方法完成原目标类的广播事件职责。

我们基于Spring的观察者模式继续改造我们的案例。

购物车事件:

/**
 * 购物车事件
 * @author tcy
 * @Date 19-09-2022
 */
@Component
public class EventShopCart extends ApplicationEvent {

    private String orderId;

    public EventShopCart(Object source, String orderId) {
        super(source);
        this.orderId=orderId;
    }


    public EventShopCart() {
        super(1);
    }
}

发布者(模拟Spring调用监听器的方法,实际开发不需要写):

/**
 * 发布者
 * @author tcy
 * @Date 19-09-2022
 */
@Component
public class MyPublisher implements ApplicationContextAware {
    private ApplicationContext applicationContext;


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * 发布事件
     * 监听该事件的监听者都可以获取消息
     *
     * @param myEvent
     */
    public void workEvent(EventShopCart myEvent) {
        //该方法会调用监听器实现的方法
        applicationContext.publishEvent(myEvent);
    }
}

监听者:

/**
 * 监听者
 * @author tcy
 * @Date 19-09-2022
 */
@Component
public class ListenerShopCart implements ApplicationListener<EventShopCart> {
    @Override
    public void onApplicationEvent(EventShopCart myEvent) {
        System.out.println("清除购物车成功...");
    }
}

Client模拟调用:

/**
 * @author tcy
 * @Date 19-09-2022
 */
public class Client {

    public static void main(String[] args) {
        ApplicationContext ac =new AnnotationConfigApplicationContext("cn.sky1998.behavior.observer.spring");

        System.out.println("订单提交成功...");
        MyPublisher bean = ac.getBean(MyPublisher.class);
        EventShopCart myEvent = ac.getBean(EventShopCart.class);

        bean.workEvent(myEvent);
    }
}

通过Spring实现观察者模式比我们手动写简单的多。

使用Spring实现观察者模式时,观察者接口、目标接口、目标实现,我们都不需要管,只负责继承ApplicationEvent类定义我们自己的事件,并实现ApplicationListener<自定义事件>接口实现我们的观察者,并在对应的业务中调用applicationContext.publishEvent(new ShopCartEvent(cmOrderItemList)),即实现了观察者模式。

读者可以拉取完整代码本地学习,实现代码均测试通过上传到 码云

五、总结

Spring使用观察者模式我在很久之前就使用过,但是并不清楚为什么要这样写,学了观察者模式以后,写起来变得通透多了。

虽然观察者模式的概念是:一对多的依赖关系,但不一定观察者有多个才能使用,我们的例子都是使用的一个观察者。

它很好的降低了目标与观察者之间的耦合关系,目标与观察者建立一套触发机制,也让他成为了最常见的设计模式。

设计模式的学习要成体系,推荐你看我往期发布的设计模式文章。

一、设计模式概述

二、设计模式之工厂方法和抽象工厂

三、设计模式之单例和原型

四、设计模式之建造者模式

五、设计模式之代理模式

六、设计模式之适配器模式

七、设计模式之桥接模式

八、设计模式之组合模式

九、设计模式之装饰器模式

十、设计模式之外观模式

十一、外观模式之享元模式

十二、设计模式之责任链模式

十三、设计模式之命令模式

十四、设计模式之解释器模式

十五、设计模式之迭代器模式

十六、设计模式之中介者模式

十七、设计模式之备忘录模式