设计模式-行为模式-观察者模式

15 Jun 2021

Reading time ~26 minutes

意图

观察者模式是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个“观察”该对象的其他对象。

image-20210615142946467

问题

假如你有两种类型的对象:顾客和商店。顾客对某个特定品牌的产品非常感兴趣,而该产品很快将会在商店里出售。

顾客可以每天来商店看看产品是否到货。但如果商品尚未到货,绝大多数来商店的顾客都会空手而归。

image-20210615143136172

另一方面,每次新产品到货时,商店可以向所有顾客发送邮件。这样,部分顾客就无需反复前往商店了,但也可能会惹恼对新产品没有兴趣的其他顾客。

我们似乎遇到了一个矛盾:要么让顾客浪费时间检查产品是否到货,要么让商店浪费资源去通知没有需求的顾客。

解决方案

拥有一些值得关注的状态的对象通常被称为目标,由于它要将自身的状态改变通知给其他对象,我们也将其称为发布者(publisher)。所有希望关注发布者状态变化的其他对象被称为订阅者(subscribers)。

观察者模式建议你为发布者类添加订阅机制,让每个对象都能订阅或取消订阅发布者事件流。不要害怕!这并不像听上去那么复杂。实际上,该机制包括:

  1. 一个用于存储订阅者对象引用的列表成员变量;
  2. 几个用于添加或删除该列表中订阅者的公有方法。

image-20210615143811809

现在,无论何时发生了重要的发布者事件,它都要遍历订阅者并调用其对象的特定通知方法。

实际应用中可能会有十几个不同的订阅这类跟踪着同一个发布者类的事件,你不会希望发布者与所有这些类相耦合的。此外如果他人会使用发布者,那么你甚至可能对其中的一些类一无所知。

因此,所有订阅者都必须实现同样的接口,发布者仅通过该接口与订阅者交互。接口中必须声明通知方法及其参数,这样发布者在发出通知时还能传递一些上下文数据。

image-20210615144159817

如果你的应用中有多个不同类型的发布者,且希望订阅者可兼容所有发布者,那么你甚至可以进一步让所有订阅者遵循同样的接口。该接口仅需描述几个订阅方法即可。这样订阅者就能在不与具体发布者耦合的情况下通过接口观察发布者的状态。

真实世界类比

image-20210615144355273

如果你订阅了一份杂志或报纸,那就不需要再去报摊查询新出版的刊物了。出版社会在刊物出版后直接将最新一期寄送至你的邮箱中。

出版设负责维护订阅者列表,了解订阅者对哪些刊物感兴趣。当订阅者希望出版社停止寄送新一期的杂志时,他们可随时从该列表中退出。

观察者模式结构

image-20210615144551567

观察者模式适合应用场景

当一个对象状态的改变需要改变其他对象,或实际对象是事先未知的或动态变化的时,可使用观察者模式。

当你使用图形用户界面类时通常会遇到一个问题。比如,你创建了自定义按钮类并允许客户端在按钮中注入自定义代码,这样当用户按下按钮时就会触发这些按钮。

观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。你可在按钮中添加订阅机制,允许客户端通过自定义订阅类注入自定义代码。

当应用中的一些对象必须观察其他对象时,可使用该模式。但仅能在有限时间内或特定情况下使用。

订阅列表是动态的,因此订阅者可随时加入或离开该列表。

实现方式

  1. 仔细检查你的业务逻辑, 试着将其拆分为两个部分: 独立于其他代码的核心功能将作为发布者; 其他代码则将转化为一组订阅类。

  2. 声明订阅者接口。 该接口至少应声明一个 update方法。

  3. 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。 记住发布者必须仅通过订阅者接口与它们进行交互。

  4. 确定存放实际订阅列表的位置并实现订阅方法。 通常所有类型的发布者代码看上去都一样, 因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。 具体发布者会扩展该类从而继承所有的订阅行为。

    但是, 如果你需要在现有的类层次结构中应用该模式, 则可以考虑使用组合的方式: 将订阅逻辑放入一个独立的对象, 然后让所有实际订阅者使用该对象。

  5. 创建具体发布者类。 每次发布者发生了重要事件时都必须通知所有的订阅者。

  6. 在具体订阅者类中实现通知更新的方法。 绝大部分订阅者需要一些与事件相关的上下文数据。 这些数据可作为通知方法的参数来传递。

    但还有另一种选择。 订阅者接收到通知后直接从通知中获取所有数据。 在这种情况下, 发布者必须通过更新方法将自身传递出去。 另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。

  7. 客户端必须生成所需的全部订阅者, 并在相应的发布者处完成注册工作。

观察者模式优缺点

优点:

  • 开闭原则。你无需修改发布者代码就能引入新的订阅者类(如果是发布者接口则可轻松引入发布者类)。
  • 你可以在运行时建立对象之间的联系。

缺点:

  • 订阅者的通知顺序是随机的。

与其他模式的关系

  • 责任链模式、命令模式、中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序来请求动态传递给一系列的潜在接收者,直至其中一名接收者对请求进行处理
    • 命令在发送者和请求者之间建立单向连接
    • 中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通
    • 观察者允许接收者动态地订阅或取消接受请求
  • 中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来看看如何做到这一点。

    中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。

    有一种流行的中介者模式实现方式依赖于观察者。 中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。 当中介者以这种方式实现时, 它可能看上去与观察者非常相似。

    当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例如, 你可永久性地将所有组件链接到同一个中介者对象。 这种实现方式和观察者并不相同, 但这仍是一种中介者模式。

    假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。 这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者。

以上摘自REFACTORING GURU

代码示例

订阅者接口:

public interface IFan {
    /**
     * 更新
     *
     * @param message
     */
    void update(String message);
}

具体订阅者:

public class Fan implements IFan{
    private String name;

    public Fan(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + "收到了消息:" + message);
    }
}

发布者接口:

public interface IIdol {
    /**
     * 添加
     *
     * @param fan
     */
    void add(IFan fan);

    /**
     * 删除
     *
     * @param fan
     */
    void delete(IFan fan);

    /**
     * 通知
     *
     * @param message
     */
    void notifyAll(String message);
}

具体发布者:

public class Idol implements IIdol{
    private List<IFan> fans = new ArrayList<>();

    @Override
    public void add(IFan fan) {
        fans.add(fan);
    }

    @Override
    public void delete(IFan fan) {
        fans.remove(fan);
    }

    @Override
    public void notifyAll(String message) {
        fans.stream().forEach(i -> i.update(message));
    }
}

客户端:

public class Demo {
    public static void main(String[] args) {
        IIdol idol = new Idol();

        IFan fan1 = new Fan("1");
        IFan fan2 = new Fan("2");
        IFan fan3 = new Fan("3");

        idol.add(fan1);
        idol.add(fan2);
        idol.add(fan3);

        idol.notifyAll("我要开演唱会了");
    }
}

//1收到了消息:我要开演唱会了
//2收到了消息:我要开演唱会了
//3收到了消息:我要开演唱会了


Design PatternsProfessional Knowledge Share Tweet +1