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

观察者设计模式

问题

假如你有两种类型的对象 顾客 商店 。 顾客对某个特定品牌的产品非常感兴趣例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售

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

访问商店或发送垃圾邮件

前往商店和发送垃圾邮件

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

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

解决方案

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

观察者模式建议你为发布者类添加订阅机制让每个对象都能订阅或取消订阅发布者事件流不要害怕这并不像听上去那么复杂实际上该机制包括 1一个用于存储订阅者对象引用的列表成员变量2几个用于添加或删除该列表中订阅者的公有方法

订阅机制

订阅机制允许对象订阅事件通知

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

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

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

通知方法

发布者调用订阅者对象中的特定通知方法来通知订阅者

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

真实世界类比

杂志和报纸订阅

杂志和报纸订阅

如果你订阅了一份杂志或报纸那就不需要再去报摊查询新出版的刊物了出版社即应用中的发布者”) 会在刊物出版后甚至提前直接将最新一期寄送至你的邮箱中

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

伪代码

publisher/EventManager.java: 基础发布者

package refactoring_guru.observer.example.publisher;

import refactoring_guru.observer.example.listeners.EventListener;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class EventManager {
    Map<String, List<EventListener>> listeners = new HashMap<>();

    public EventManager(String... operations) {
        for (String operation : operations) {
            this.listeners.put(operation, new ArrayList<>());
        }
    }

    public void subscribe(String eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.add(listener);
    }

    public void unsubscribe(String eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.remove(listener);
    }

    public void notify(String eventType, File file) {
        List<EventListener> users = listeners.get(eventType);
        for (EventListener listener : users) {
            listener.update(eventType, file);
        }
    }
}

editor/Editor.java:具体发布者由其他对象追踪

package refactoring_guru.observer.example.editor;

import refactoring_guru.observer.example.publisher.EventManager;

import java.io.File;

public class Editor {
    public EventManager events;
    private File file;

    public Editor() {
        this.events = new EventManager("open", "save");
    }

    public void openFile(String filePath) {
        this.file = new File(filePath);
        events.notify("open", file);
    }

    public void saveFile() throws Exception {
        if (this.file != null) {
            events.notify("save", file);
        } else {
            throw new Exception("Please open a file first.");
        }
    }
}

listeners/EventListener.java: 通用观察者接口

package refactoring_guru.observer.example.listeners;

import java.io.File;

public interface EventListener {
    void update(String eventType, File file);
}

listeners/EmailNotificationListener.java: 收到通知后发送邮件

package refactoring_guru.observer.example.listeners;

import java.io.File;

public class EmailNotificationListener implements EventListener {
    private String email;

    public EmailNotificationListener(String email) {
        this.email = email;
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Email to " + email + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }
}

listeners/LogOpenListener.java: 收到通知后在日志中记录一条消息

package refactoring_guru.observer.example.listeners;

import java.io.File;

public class LogOpenListener implements EventListener {
    private File log;

    public LogOpenListener(String fileName) {
        this.log = new File(fileName);
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Save to log " + log + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }
}

Demo.java: 初始化代码

package refactoring_guru.observer.example;

import refactoring_guru.observer.example.editor.Editor;
import refactoring_guru.observer.example.listeners.EmailNotificationListener;
import refactoring_guru.observer.example.listeners.LogOpenListener;

public class Demo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.events.subscribe("open", new LogOpenListener("/path/to/log/file.txt"));
        editor.events.subscribe("save", new EmailNotificationListener("admin@example.com"));

        try {
            editor.openFile("test.txt");
            editor.saveFile();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

观察者模式适合应用场景

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

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

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

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

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

实现方式

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

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

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

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

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

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

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

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

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

观察者模式优缺点

  • 开闭原则你无需修改发布者代码就能引入新的订阅者类如果是发布者接口则可轻松引入发布者类)。

  • 你可以在运行时建立对象之间的联系

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

与其他模式的关系

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

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。

    • 命令在发送者和请求者之间建立单向连接。

    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。

    • 观察者允许接收者动态地订阅或取消接收请求。

  • 中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式而有时可以同时使用让我们来看看如何做到这一点

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

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

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

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