设计模式之观察者模式

概念

观察者模式(Observer Pattern)是一种行为型设计模式,它定义了对象之间的一对多依赖关系 :当一个对象(称为 "主题" 或 "被观察者")的状态发生变化时,所有依赖于它的对象(称为 "观察者")都会自动收到通知并进行更新。该模式的核心是解耦主题和观察者,让它们可以独立变化,同时保持联动。

举一个简单的例子:

假设小区附近有一个报社(主题/被观察者 ),小区居民(观察者 )都向该报社订阅报纸,报社记录下了订阅居民们的地址(注册观察者)。

每天报社都会更新报纸的内容(主题状态发生变化 ),更新之后报社会根据订阅者的地址统一将报纸送到订阅者的家门口(主题自动通知所有观察者)。

你收到报纸之后,可能会比较关心体育版块,而邻居老王则比较关心时政,楼下的小李则会重点关注报纸上的招聘信息(观察者们收到通知后,根据自身需求做出不同响应)。

某天,你因为一些原因不再需要订阅这个报纸,于是打电话给报社取消了订阅(从观察者列表中移除)。之后报社不再给你送报纸,但是并不会影响其他居民继续收到报纸。

结构组成

上述"报社"的例子中,可清晰梳理出观察者模式的两种核心角色:

  • 主题/被观察者
    1. 它是被观察的对象。主题维护一个观察者列表,提供添加、移除观察者的方法,以及通知所有观察者列表内的观察者的方法。
    2. 当主题自身状态发生改变的时候,它会主动调用通知方法,触发所有观察者进行更新操作。
  • 观察者
    1. 接收主题通知的对象。它定义一个更新方法,当收到主题的状态变更通知时,通过该方法执行具体的更新操作。

而在实际开发中,为解决耦合性、扩展性和复用性问题,需对上述角色进行抽象,形成两层结构:

  • 抽象主题/抽象被观察者(Subject):定义主题的通用接口,包括管理观察者(添加、移除)和通知观察者的方法,不涉及具体业务逻辑,为具体主题提供规范。
  • 抽象观察者Observer):定义观察者的通用接口,包含更新方法的抽象声明,具体观察者需实现该方法以完成特定业务逻辑。

通过抽象层的设计,具体主题和具体观察者可独立演化,只需遵循抽象接口规范即可实现交互,极大提升了系统的灵活性和可维护性。

工作流程

同样,我们从"报社"一例中能梳理出各角色之间的工作流程:

  1. 观察者向主题进行注册(添加到观察者列表);
  2. 主题状态发生变化,调用自身通知方法;
  3. 通知方法遍历观察者列表,调用观察者的更新方法;
  4. 观察者通过更新方法获取主题的状态变化,执行相应操作;
  5. 观察者可随时从主题中注销,不再接收通知。

示例

以"报社"为例,编写实现观察者模式的完整示例代码。

UML

  1. 接口定义
    • Subject接口声明了注册、移除和通知观察者的方法。
    • Observer接口定义了update()方法,用于接收状态更新通知。
  2. 具体主题:NewspaperOffice
    • 继承并实现Subject接口,维护一个residents观察者列表。
    • setNewspaperContent()方法触发状态更新,调用notifyObservers()遍历列表通知所有观察者。
  3. 具体观察者:Resident
    • 实现Observer接口的update()方法,处理报纸内容。
    • 通过subscribe()/unsubscribe()管理订阅关系,使用weak_ptr避免循环引用。
    • 包含居民属性(name,interest),体现个性化响应能力。
  4. 关系设计
    • 聚合关系NewspaperOffice聚合多个Observer,生命周期独立(居民取消订阅不影响报社存在)。
    • 依赖关系Resident依赖Subject接口实现订阅管理,符合依赖倒置原则。

C++实现

C++ 复制代码
#include <iostream>
#include <utility>
#include <vector>
#include <string>
#include <algorithm>
#include <memory>

// 前向声明
class Resident;

// 观察者接口
class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(const std::string& newspaperContent) = 0;
};

// 主题接口
class Subject {
public:
    virtual ~Subject() = default;
    virtual void registerObserver(const std::shared_ptr<Observer>& observer) = 0;
    virtual void removeObserver(const std::shared_ptr<Observer>& observer) = 0;
    virtual void notifyObservers() = 0;
};

// 具体主题:报社
class NewspaperOffice : public Subject {
private:
    std::vector<std::shared_ptr<Observer>> residents;
    std::string newspaperContent;

public:
    void registerObserver(const std::shared_ptr<Observer>& observer) override {
        residents.push_back(observer);
        std::cout << "新居民订阅了报纸\n";
    }

    void removeObserver(const std::shared_ptr<Observer>& observer) override {
        auto it = std::find(residents.begin(), residents.end(), observer);
        if (it != residents.end()) {
            residents.erase(it);
            std::cout << "一位居民取消了报纸订阅\n";
        }
    }

    void notifyObservers() override {
        for (const auto& resident : residents) {
            resident->update(newspaperContent);
        }
    }

    void setNewspaperContent(const std::string& content) {
        newspaperContent = content;
        std::cout << "\n报社更新了报纸内容: " << content << "\n";
        notifyObservers();
    }
};

// 具体观察者:居民
class Resident : public Observer, public std::enable_shared_from_this<Resident> {
private:
    std::string name;
    std::string interest;
    std::weak_ptr<Subject> newspaperOffice;

public:
    Resident(std::string  name, std::string  interest)
            : name(std::move(name)), interest(std::move(interest)) {}

    void update(const std::string& newspaperContent) override {
        std::cout << name << "收到了报纸,正在查看" << interest << "版块\n";
        // 这里可以添加根据兴趣处理报纸内容的逻辑
    }

    void subscribe(const std::shared_ptr<Subject>& office) {
        newspaperOffice = office;
        office->registerObserver(shared_from_this());
    }

    void unsubscribe() {
        if (auto office = newspaperOffice.lock()) {
            office->removeObserver(shared_from_this());
        }
    }

    std::string getName() const { return name; }
    std::string getInterest() const { return interest; }
};

int main() {
    // 创建报社
    auto newspaperOffice = std::make_shared<NewspaperOffice>();

    // 创建居民
    auto resident1 = std::make_shared<Resident>("小明", "体育");
    auto resident2 = std::make_shared<Resident>("老王", "时政");
    auto resident3 = std::make_shared<Resident>("小李", "招聘");

    // 居民订阅报纸
    resident1->subscribe(newspaperOffice);
    resident2->subscribe(newspaperOffice);
    resident3->subscribe(newspaperOffice);

    std::cout << "\n";

    // 报社更新内容
    newspaperOffice->setNewspaperContent("今日头条:本地球队赢得比赛");

    std::cout << "\n";

    // 小明取消订阅
    resident1->unsubscribe();

    std::cout << "\n";

    // 报社再次更新内容
    newspaperOffice->setNewspaperContent("重要新闻:新政策出台");

    return 0;
}

运行结果如下:

Java实现

Java 复制代码
import java.util.ArrayList;
import java.util.List;

// 观察者接口
interface Observer {
    void update(String newspaperContent);
}

// 主题接口
interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

// 具体主题:报社
class NewspaperOffice implements Subject {
    private List<Observer> residents;
    private String newspaperContent;

    public NewspaperOffice() {
        this.residents = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer observer) {
        residents.add(observer);
        System.out.println("新居民订阅了报纸");
    }

    @Override
    public void removeObserver(Observer observer) {
        if (residents.remove(observer)) {
            System.out.println("一位居民取消了报纸订阅");
        }
    }

    @Override
    public void notifyObservers() {
        for (Observer resident : residents) {
            resident.update(newspaperContent);
        }
    }

    public void setNewspaperContent(String content) {
        this.newspaperContent = content;
        System.out.println("\n报社更新了报纸内容: " + content);
        notifyObservers();
    }
}

// 具体观察者:居民
class Resident implements Observer {
    private String name;
    private String interest;
    private Subject newspaperOffice;

    public Resident(String name, String interest) {
        this.name = name;
        this.interest = interest;
    }

    @Override
    public void update(String newspaperContent) {
        System.out.println(name + "收到了报纸,正在查看" + interest + "版块");
    }

    public void subscribe(Subject office) {
        this.newspaperOffice = office;
        office.registerObserver(this);
    }

    public void unsubscribe() {
        if (newspaperOffice != null) {
            newspaperOffice.removeObserver(this);
            newspaperOffice = null;
        }
    }

    public String getName() {
        return name;
    }

    public String getInterest() {
        return interest;
    }
}

// 测试类
public class ObserverPatternExample {
    public static void main(String[] args) {
        // 创建报社
        NewspaperOffice newspaperOffice = new NewspaperOffice();

        // 创建居民
        Resident resident1 = new Resident("小明", "体育");
        Resident resident2 = new Resident("老王", "时政");
        Resident resident3 = new Resident("小李", "招聘");

        // 居民订阅报纸
        resident1.subscribe(newspaperOffice);
        resident2.subscribe(newspaperOffice);
        resident3.subscribe(newspaperOffice);

        System.out.println();

        // 报社更新内容
        newspaperOffice.setNewspaperContent("今日头条:本地球队赢得比赛");

        System.out.println();

        // 小明取消订阅
        resident1.unsubscribe();

        System.out.println();

        // 报社再次更新内容
        newspaperOffice.setNewspaperContent("重要新闻:新政策出台");
    }
}

延伸思考

现在我们已经了知道观察者模式的大致原理,但也能轻易地想到,上述例子中主题的通知是针对所有观察者的,但在实际应用过程中,我们可能并不需要通知所有观察者,而是根据实际情况按需通知,所以就需要一个能够筛选观察者的机制。

下面是几个可行的方法:

  1. 基于主题状态的筛选:主题可以在通知前检查自身状态,决定是否通知特定观察者。
  • 优点:
    • 实现简单:不需要改变观察者接口,只需在主题内部添加判断逻辑
    • 集中控制:所有筛选逻辑集中在主题中,便于统一管理和修改
    • 低耦合:观察者无需了解筛选逻辑,保持了观察者的简单性
  • 缺点
    • 主题职责过重:主题需要处理所有筛选逻辑,违反了单一职责原则
    • 缺乏灵活性:新增筛选条件需要修改主题代码,不符合开闭原则
    • 可扩展性差:当筛选条件复杂时,主题代码会变得臃肿难以维护
  1. 观察者注册时添加元数据:观察者注册时可以附带筛选条件,主题在通知时检查这些条件。
  • 优点:
    • 高度灵活:每个观察者可以自定义筛选条件,支持多样化需求
    • 职责分离:筛选逻辑分散到各观察者注册时,主题只需执行筛选
    • 易于扩展:新增筛选类型不需要修改主题核心代码
  • 缺点:
    • 接口复杂化:观察者注册接口需要支持筛选条件参数
    • 性能开销:每次通知都需要对每个观察者执行筛选函数
    • 条件管理复杂:需要维护观察者与筛选条件的映射关系
  1. 使用中间过滤器:在主题和观察者之间添加过滤器层,处理通知的分发逻辑。
  • 优点:
    • 职责清晰:主题和观察者完全解耦,都不需要关心筛选逻辑
    • 高度可配置:可以动态更换过滤器,实现不同的筛选策略
    • 复用性强:同一过滤器可以用于多个主题-观察者关系
  • 缺点:
    • 系统复杂性增加:引入了新的组件(过滤器),增加了架构复杂度
    • 性能开销:多了一层调用,可能影响通知效率
    • 调试困难:问题排查时需要跟踪经过过滤器的调用链
  1. 分主题实现:创建多个特定主题,观察者只订阅相关主题。
  • 优点:
    • 高效直接:观察者只订阅感兴趣的主题,无需运行时筛选
    • 职责明确:每个主题职责单一,符合单一职责原则
    • 性能最优:没有运行时筛选开销,通知直接发送给相关观察者
  • 缺点:
    • 主题爆炸:可能导致大量细粒度主题类,增加系统复杂度
    • 观察者管理复杂:观察者需要管理多个订阅关系
    • 灵活性有限:难以实现动态变化的筛选条件

实现方法多种多样,也能结合其他设计模式,多维度考量实际需求,上述4种方法一般可以根据如下情况进行选择:

  1. 简单系统:优先考虑基于主题状态的筛选或分主题实现
  2. 中等复杂度系统:考虑观察者注册时添加元数据的方式
  3. 复杂企业系统:使用中间过滤器模式,提供最大的灵活性
  4. 高性能要求系统:优先选择分主题实现,避免运行时筛选开销

实际应用中,这些模式也可以组合使用。例如,可以先使用分主题实现大类筛选,再在主题内使用基于状态的筛选进行细粒度控制,或者在过滤器链中组合多个筛选策略。

设计原则

  1. 开闭原则 (Open/Closed Principle - OCP) :软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
    • 对扩展开放 :你可以轻松地添加新的观察者(ConcreteObserver)来响应主题(Subject)的状态变化,而无需修改主题的代码。例如,在天气站应用中,你可以新增一个"天气预警系统"作为观察者,而无需改动"天气数据"主题本身。
    • 对修改关闭:主题类的核心逻辑(如维护观察者列表、通知观察者)是稳定的,不需要因为观察者数量的增加或类型的改变而被修改。
  2. 依赖倒置原则 (Dependency Inversion Principle - DIP) :高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
    • 观察者模式通过引入抽象主题(Subject抽象观察者(Observer 接口来实现这一原则。
    • 具体主题(ConcreteSubject 依赖于抽象接口 Observer,而不是依赖于任何具体的观察者类。它只知道有一组对象实现了 Observer 接口,并会调用它们的 update 方法。
    • 具体观察者(ConcreteObserver 也依赖于抽象接口 Subject(或 ConcreteSubject),它通过接口来获取所需的状态,而不是直接依赖具体实现的细节。
    • 这样,双方都依赖于抽象,而不是具体实现,极大地降低了模块间的耦合度。
  3. 封装变化原则(Encapsulate What Varies) :识别系统中变化的方面,并将其与不变的方面分离开。
    • 在观察者模式中,变化的是观察者的数量、类型以及它们的行为 。而不变的是主题管理观察者和通知它们的机制
    • 模式将这种变化的部分(即谁需要被通知、通知后做什么)封装在了抽象的 Observer 接口之后。主题只负责触发通知,完全不管后续发生了什么变化。

优缺点

优点

  1. 松耦合 (Loose Coupling)
    • 主题和观察者之间是抽象耦合的,而非具体耦合。
    • 主题只知道观察者实现了某个接口(例如 IObserver),而不知道观察者的具体类是谁、做了什么。这符合"依赖倒置原则"。因此,可以独立地复用主题或观察者,修改其中一方只要接口不变,就不会直接影响另一方。
  2. 强大的扩展性
    • 可以随时在运行时动态地添加新的观察者或移除已有的观察者,而无需修改主题的代码。这符合"开闭原则"(对扩展开放,对修改关闭)。
    • 新的观察者类型可以随时加入系统,主题无需关心观察者的数量或具体类型。
  3. 建立了一种触发/响应机制
    • 它完美地支持了"事件驱动架构"。主题的状态变化是事件,观察者的更新方法是事件处理器。
    • 这种机制可以用于实现分布式系统中的事件处理系统、MVC架构中模型和视图的联动(模型变化自动通知视图更新)等。
  4. 信息传递的直接性
    • "推模型[1](#1)"(由主题将数据推送给观察者)的实现方式可以让观察者立即获得最新的、相关的数据,而不需要再反向查询主题的状态。

缺点

  1. 通知顺序的不确定性
    • 当一个主题有多个观察者时,观察者被通知的顺序可能是不可预测的(通常取决于它们被添加的顺序)。如果观察者之间有依赖关系,或者通知顺序对业务逻辑至关重要,这会导致难以调试的复杂问题。
  2. 性能开销
    • 如果观察者数量非常多,或者每个观察者的更新操作非常耗时,遍历整个观察者列表并进行通知的过程会带来显著的性能开销。此外,某些无用的更新(例如主题的某个细微变化与某个观察者无关)也会被触发。
  3. 循环依赖和无限循环的风险
    • 如果观察者在处理更新通知时,又间接地导致了主题状态的再次改变(从而再次触发通知),就可能形成一个无限循环的通知链,导致系统崩溃。
  4. 内存泄漏的风险(特别是在某些语言中)
    • 主题持有所有观察者的引用。如果观察者被创建后没有正确地取消注册(退订),那么即使程序逻辑上已经不再需要它,由于主题仍引用着它,垃圾回收器(在Java、C#等语言中)也无法回收其内存。这被称为"隐性依赖"或"lapsed listener problem"。
    • 在使用时,必须牢记:注册和注销要成对出现
  5. 观察者可能被忽略的更新细节
    • 在简单的"推模型"实现中,主题通常会将所有数据推送给所有观察者,观察者可能收到不关心的信息。
    • 而在"拉模型"中(观察者接到通知后主动从主题拉取数据),观察者又需要知道主题的接口细节,这增加了一定的耦合度,也可能导致观察者拉取了过多不必要的数据。

在许多现代语言和框架(如 .NET 的 event、Java 的 PropertyChangeEvent、RxJS等)中,观察者模式的思想已经被封装成了更安全、更易用的事件系统,应优先考虑使用这些内置机制。

注意事项

  1. 生命周期管理与内存泄漏 :主题持有对观察者的强引用。如果观察者(例如,一个 UI 界面或业务对象)在不需要时没有从主题中注销,垃圾回收器(GC)就无法回收它,导致内存泄漏。

    对策:

    • 谁创建,谁销毁 :严格遵守这一原则。在观察者的生命周期结束时(例如,在 dispose()close() 或析构函数中),必须主动调用主题的 unregister/removeListener 方法。
    • 使用弱引用(Weak Reference) :在某些语言(如 Java、C#)中,主题可以使用 WeakReference 来持有观察者。这样,当观察者不被其他任何地方引用时,GC 可以正常回收它,主题会自动清理无效的引用。但要注意,这会使得通知顺序和时机更加不确定。
    • 自动化的监听器管理:在拥有强大垃圾回收机制的语言中,注意循环引用问题。
  2. 通知顺序的不确定性:主题通知观察者的顺序通常是其被添加的顺序,但依赖这个顺序是非常脆弱的设计。如果未来添加顺序发生变化,或使用了弱引用等机制,程序行为可能会被破坏。

    对策:

    • 不要依赖通知顺序:理想情况下,每个观察者的处理应该是独立的、无状态的。这是最安全的选择。
    • 如果顺序必须确定:在主题中引入优先级机制。让观察者在注册时提供优先级参数,主题内部按优先级排序后再通知。但这会增加设计的复杂性。
  3. 错误处理与稳定性 :果在遍历通知观察者的过程中,某个观察者的 update 方法抛出了异常,会发生什么?这可能会导致后续的观察者收不到通知,甚至整个通知流程被中断。

    对策:

    • 将通知循环包裹在 try-catch 中 :主题的通知方法 (notifyObservers) 应该捕获单个观察者的异常,记录下来(日志),并继续通知下一个观察者。绝不能因为一个观察者的错误而影响其他观察者。
    • 使用"托管"的事件系统:许多框架(如 .NET、RxJava)的事件机制已经内置了良好的错误处理,可以考虑使用。
  4. 性能开销:默认的同步通知方式意味着主题要等待所有观察者处理完毕,整个过程是阻塞的。如果某个观察者处理很慢,会拖慢主题乃至整个线程;遍历一个非常庞大的观察者列表本身就有成本。

    对策:

    • 异步通知:将通知过程改为异步(例如,将事件放入消息队列,或使用线程池)。这可以解耦并提高主题的响应速度,但会引入线程安全和事件顺序等新问题。
    • 细化事件类型:不要总是通知"我变了",而是可以通知"我的XXX属性变了"。观察者只订阅它们真正关心的事件,减少不必要的通知和处理。
  5. 保证数据的一致性:观察者在被通知时,看到的主题状态可能已经和触发通知的那一刻不同了,特别是在多线程环境下。

    对策:

    • 在主题加锁期间进行通知?(坏主意):这极易导致死锁。因为观察者可能在更新时试图获取其他锁。
    • 传递事件快照(Snapshot):在通知前,主题将状态数据封装在一个不可变(immutable)的事件对象中。这样,所有观察者看到的就是触发通知时那一刻状态的快照,保证了数据视图的一致性。这是"推模型"的推荐做法。
  6. 避免在通知中修改主题 :观察者在它的 update 方法中,又调用了主题的方法,可能导致主题状态再次改变,从而触发新一轮的通知,形成无限递归或非常复杂的调用链。

    对策:

    • 代码审查和设计约束:在设计和代码审查时明确禁止或警惕这种"反向调用"。如果业务确实需要,必须非常小心地设计,有时可以通过设置标志位来避免重入。
  7. 文档化:清晰地记录哪些主题会发出哪些事件,以及观察者应该期望什么。这对于团队协作和后期维护至关重要。

  8. 明确选择推模型、拉模型

应用场景

  1. GUI 事件处理(如按钮点击、输入框变化)
  • 场景描述:在图形用户界面(GUI)开发中,用户操作(如点击按钮、输入文本、移动鼠标)会触发事件,需要通知多个监听器执行响应操作。
  • 例子
    • Java Swing/AWT 中的 ActionListener:按钮(被观察者)维护一个观察者列表(监听器),当点击事件发生时,通知所有注册的监听器执行动作。
    • Android 中的 OnClickListener:类似机制处理视图交互。
  • 优势:解耦UI组件和业务逻辑,允许动态添加或移除事件处理程序。

  1. 消息队列/发布-订阅系统(如 Kafka、Redis Pub/Sub)
  • 场景描述:消息中间件使用观察者模式的变体(发布-订阅模式),生产者发布消息到主题(Topic),多个消费者订阅该主题并接收消息。
  • 例子
    • Kafka:生产者向Topic发送消息,多个消费者组订阅同一Topic并独立消费消息。
    • Redis Pub/Sub:通过 SUBSCRIBE 命令订阅频道,发布者通过 PUBLISH 向频道发送消息。
  • 优势:支持异步通信、系统解耦、水平扩展消费者。

  1. 数据绑定与响应式编程(如 Vue.js、React)
  • 场景描述:前端框架中,数据模型(被观察者)的变化需要自动同步到视图(观察者),实现双向数据绑定。
  • 例子
    • Vue.js 的响应式原理:通过 Object.definePropertyProxy 监听数据变化,通知依赖的视图组件更新。
    • React 的状态管理(如Context API+useState):状态变更时通知使用该状态的组件重新渲染。
  • 优势:减少手动DOM操作,提高开发效率,保持数据与UI的一致性。

  1. 分布式系统中的事件通知(如微服务间通信)
  • 场景描述:微服务架构中,一个服务的状态变化(如订单创建)需要通知其他服务(如库存服务、日志服务)。
  • 例子
    • 使用事件总线(Event Bus):服务发布事件,其他服务订阅这些事件(如Spring Cloud Stream、RabbitMQ)。
    • 数据库变更监听:通过CDC(Change Data Capture)工具(如Debezium)捕获数据库变更并通知下游服务。
  • 优势:服务间解耦,避免直接API调用,提高系统可扩展性。

  1. 监控与报警系统
  • 场景描述:当系统指标(如CPU使用率、错误日志)达到阈值时,需要通知多个渠道(邮件、短信、钉钉)。
  • 例子
    • Prometheus监控:配置报警规则(被观察者),当规则触发时通知Alertmanager(观察者),后者分发给不同接收器。
    • 自定义监控脚本:检测到异常时调用注册的报警处理器。
  • 优势:灵活添加或修改报警策略和接收方。

  1. 游戏开发(如角色状态变化触发UI更新)
  • 场景描述:游戏中角色属性(血量、金币)变化时,需要同步更新多个UI元素(血条、数字显示、音效)。
  • 例子
    • Unity游戏引擎:通过C#事件或委托实现观察者模式,当角色受伤时通知UI组件刷新。
    • 自定义事件系统:玩家成就解锁时通知统计模块、弹窗提示和存档系统。
  • 优势:避免强耦合,方便扩展游戏功能。

  1. 设计模式中的典型应用(如MVC架构)
  • 场景描述:在MVC模式中,模型(Model)作为被观察者,视图(View)作为观察者,当模型数据变化时自动更新视图。
  • 例子
    • Java Swing的Model-View-Controller:表格模型(TableModel)变化时,通知JTable视图刷新。
    • 后端MVC框架(如Spring MVC):虽不直接使用观察者,但思想类似(通过请求响应机制)。
  • 优势:分离数据逻辑和表现层,符合单一职责原则。

  1. 日志系统或审计跟踪
  • 场景描述:当系统执行关键操作(如用户登录、数据修改)时,需要记录日志并同时发送到多个输出源(文件、数据库、控制台)。
  • 例子
    • Log4j / Logback:日志事件被发布,多个Appender(观察者)负责输出到不同目标。
    • 自定义审计模块:业务操作触发事件,通知日志记录器和安全分析模块。
  • 优势:灵活配置日志输出,无需修改业务代码。

  1. 股票行情或实时数据推送
  • 场景描述:金融应用中,股票价格变化时需要实时推送给多个客户端(App、网页、后台分析系统)。
  • 例子
    • WebSocket 服务:维护客户端连接列表(观察者),当行情数据更新时广播给所有客户端。
    • 交易所API:客户端订阅特定股票代码的变动事件。
  • 优势:减少轮询开销,实现实时数据同步。

  1. 插件系统或可扩展架构
  • 场景描述:主程序提供核心功能,允许第三方插件注册为观察者,在特定事件(如启动完成、用户操作)时执行插件逻辑。
  • 例子
    • IDE(如VSCode、Eclipse)的插件机制:IDE触发事件(如文件保存),插件监听并执行代码格式化、语法检查等。
    • 游戏模组(Mod):主游戏触发事件(如角色升级),模组响应并添加自定义行为。
  • 优势:开放扩展性,保持核心系统稳定。

  1. 推模型:主题将详细数据通过事件对象传递。效率高,但观察者可能收到不必要的数据。

    拉模型:主题只通知"我变了",观察者收到通知后主动调用主题的方法查询所需数据。更灵活,但增加了耦合(观察者需要知道主题的查询接口)和调用次数。 ↩︎