python设计模式–观察者模式(Observer Pattern)

Python 设计模式——观察者模式(Observer Pattern)详解

本文面向零基础新手,从概念到“一对多通知”,从主题与观察者接口到实际应用,全方位讲解观察者模式。


一、什么是观察者模式?

1.1 通俗理解

观察者模式是一种行为型设计模式,核心思想是:

有一个“主题”(被观察对象),状态会变化;有多个“观察者”,关心主题的变化。主题不直接调用每个观察者的代码,而是维护一个观察者列表,一旦状态变化就“通知”列表里的所有观察者(调用它们的更新方法)。这样主题和观察者解耦:主题只负责“在变化时通知”,观察者只负责“收到通知后做自己的事”。

可以这样类比:

  • 订阅公众号:公众号(主题)发新文章时,会推送给所有订阅者(观察者)。公众号不需要知道订阅者是谁、他们各自要做什么,只要在发文章时“通知所有人”;每个订阅者收到通知后自己去读文章、转发、收藏等。主题和观察者解耦,一对多通知。
  • 界面与数据:界面上的多个控件(表格、图表、标签)都显示同一份数据。数据(主题)变化时,只要“通知所有显示我的控件”,每个控件收到通知后自己去取新数据并刷新。数据源不需要知道有多少个控件、每个控件怎么刷新。

所以,观察者模式解决的是:当“一个对象状态变化,需要通知多个依赖它的对象”时,用“订阅-通知”的方式解耦主题和观察者,避免主题直接依赖所有观察者。

1.2 为什么需要观察者模式?

场景一:一对多依赖

一个数据源可能被多个界面、多个模块使用;若数据源直接调用每个使用者的“更新”方法,就要持有所有使用者的引用,耦合高且难扩展。用观察者:使用者“订阅”数据源,数据源只维护订阅列表,变化时遍历列表通知即可,主题不关心观察者具体是谁、有多少个

场景二:事件驱动、响应式

按钮点击、温度变化、订单状态变更等“事件”发生时要触发多种反应(写日志、发邮件、更新界面)。若把反应都写进主题里,主题会臃肿。用观察者:每种反应是一个观察者,主题只发“我变了”,观察者各自响应,易于增加或删除响应

场景三:界面与业务分离

业务数据(主题)变化时,多个 UI 组件(观察者)要刷新。业务层不依赖具体 UI 类,只依赖“观察者接口”;UI 层实现观察者并订阅业务数据,符合依赖倒置

1.3 适用场景(什么时候用?)

场景 说明
一个对象状态变化要通知多个对象 数据源与多个展示、事件与多个处理者。
不希望主题强依赖具体观察者 主题只依赖“观察者接口”,观察者可以随时增删。
事件驱动、发布-订阅风格 按钮事件、温度报警、消息总线等。

简单记忆一个变、多个要跟着更新,且希望主题不直接依赖具体观察者时,用观察者模式。


二、观察者模式的结构(四个角色)

角色 说明
主题(Subject) 维护一个观察者列表;提供 attach(observer)detach(observer);状态变化时调用 notify(),遍历列表让每个观察者更新。
具体主题(ConcreteSubject) 拥有实际状态(如温度、数据);状态改变时调用 notify()。
观察者(Observer) 抽象接口,通常有一个 update() 方法(或带参数,如 update(subject)、update(new_state)),供主题在 notify 时调用。
具体观察者(ConcreteObserver) 实现 update,收到通知后从主题取数据或根据参数更新自己(如刷新界面、写日志)。

关系可以理解为:

  • 主题 持有 观察者列表,不关心观察者具体类型,只要求它们有 update 方法。
  • 状态变化时,主题 notify() → 遍历观察者列表,对每个观察者调用 observer.update(…)
  • 观察者 可以持有对主题的引用,在 update 里向主题拉取最新数据(拉模型);或主题在 notify 时把新状态推送给观察者(推模型)。

三、示例一:天气站与多个显示屏(最经典)

需求:天气站(主题)有温度、湿度;多个显示屏(观察者)订阅天气站,数据变化时每个屏自动更新显示。主题不关心有多少个屏、每个屏怎么显示。

3.1 观察者接口 + 主题接口

from abc import ABC, abstractmethod

class Observer(ABC):
    @abstractmethod
    def update(self, temperature: float, humidity: float):
        pass

class Subject(ABC):
    @abstractmethod
    def attach(self, observer: Observer):
        pass

    @abstractmethod
    def detach(self, observer: Observer):
        pass

    @abstractmethod
    def notify(self):
        pass

3.2 具体主题:天气站

class WeatherStation(Subject):
    def __init__(self):
        self._observers = []
        self._temperature = 0.0
        self._humidity = 0.0

    def attach(self, observer: Observer):
        self._observers.append(observer)

    def detach(self, observer: Observer):
        if observer in self._observers:
            self._observers.remove(observer)

    def notify(self):
        for obs in self._observers:
            obs.update(self._temperature, self._humidity)

    def set_measurements(self, temperature: float, humidity: float):
        self._temperature = temperature
        self._humidity = humidity
        self.notify()

3.3 具体观察者:显示屏

class DisplayScreen(Observer):
    def __init__(self, name: str):
        self.name = name
        self.temperature = 0.0
        self.humidity = 0.0

    def update(self, temperature: float, humidity: float):
        self.temperature = temperature
        self.humidity = humidity
        print(f"  [{self.name}] 更新: 温度={temperature}, 湿度={humidity}")

3.4 使用

station = WeatherStation()
screen1 = DisplayScreen("屏1")
screen2 = DisplayScreen("屏2")
station.attach(screen1)
station.attach(screen2)

station.set_measurements(25.5, 60)
# [屏1] 更新: 温度=25.5, 湿度=60
# [屏2] 更新: 温度=25.5, 湿度=60

要点:WeatherStation 不依赖具体 DisplayScreen,只依赖 Observer 接口;增加新显示屏只需 new 一个观察者并 attach,主题代码不用改。


四、示例二:推模型 vs 拉模型

  • 推模型:主题在 notify 时把新状态作为参数传给观察者,如 observer.update(temperature, humidity)。观察者不需要持有主题引用,但接口会随“推送内容”变化。
  • 拉模型:主题在 notify 时只传自己(或事件名),如 observer.update(self);观察者在 update 里主动向主题取需要的数据。观察者需要持有主题引用,但主题的 notify 接口简单、稳定。

上面天气站示例是推模型。下面补一个拉模型写法(观察者持有主题,update 时从主题取数据):

class WeatherStation(Subject):
    # ... 同上,但 notify 改为:
    def notify(self):
        for obs in self._observers:
            obs.update(self)  # 只传 self

class DisplayScreen(Observer):
    def __init__(self, name: str):
        self.name = name

    def update(self, subject: WeatherStation):
        t = subject._temperature
        h = subject._humidity
        print(f"  [{self.name}] 更新: 温度={t}, 湿度={h}")

实际项目中可据需要选推或拉,或混合(如 push 事件类型 + pull 数据)。


五、示例三:按钮点击(事件与多个处理者)

需求:一个按钮(主题),点击时通知多个“监听器”(观察者):写日志、弹窗、发请求等。按钮不关心具体有哪些处理,只负责“点击时 notify”。

class Button:
    def __init__(self):
        self._listeners = []

    def attach(self, listener):
        self._listeners.append(listener)

    def click(self):
        print("按钮被点击")
        for L in self._listeners:
            L.on_click()

class LogListener:
    def on_click(self):
        print("  [日志] 记录点击")

class DialogListener:
    def on_click(self):
        print("  [弹窗] 显示提示")

button = Button()
button.attach(LogListener())
button.attach(DialogListener())
button.click()
# 按钮被点击
# [日志] 记录点击
# [弹窗] 显示提示

要点:这里“观察者”的接口是 on_click(),本质仍是“主题变化时通知多个观察者”;名字可按场景叫 Listener、Handler、Subscriber 等。


六、示例四:订单状态变化通知(业务场景)

需求:订单(主题)状态从“待支付”变为“已支付”时,要通知:库存扣减、发邮件、更新统计。订单不直接调库存、邮件、统计模块,而是维护观察者列表,状态变化时 notify。

class OrderObserver(ABC):
    @abstractmethod
    def on_order_paid(self, order_id: str, amount: float):
        pass

class Order:
    def __init__(self, order_id: str):
        self.order_id = order_id
        self.amount = 0.0
        self._paid = False
        self._observers = []

    def attach(self, obs: OrderObserver):
        self._observers.append(obs)

    def pay(self, amount: float):
        self.amount = amount
        self._paid = True
        for obs in self._observers:
            obs.on_order_paid(self.order_id, self.amount)

class InventoryObserver(OrderObserver):
    def on_order_paid(self, order_id: str, amount: float):
        print(f"  [库存] 扣减订单 {order_id} 对应库存")

class EmailObserver(OrderObserver):
    def on_order_paid(self, order_id: str, amount: float):
        print(f"  [邮件] 发送订单 {order_id} 支付成功通知")

order = Order("O001")
order.attach(InventoryObserver())
order.attach(EmailObserver())
order.pay(99.0)
# [库存] 扣减订单 O001 对应库存
# [邮件] 发送订单 O001 支付成功通知

要点:Order 不依赖 Inventory、Email 具体类,只依赖 OrderObserver;新增一种通知方式只需加一个观察者并 attach。


七、观察者模式 vs 其他

模式 目的 与观察者的区别
观察者 主题变化时通知多个观察者,一对多 主题持有观察者列表,直接调用观察者 update。
中介者 多对象通过中介者协作,减少两两引用 中介者是“多对多”的协调中心;观察者是“一个主题多观察者”。
发布-订阅 发布者和订阅者通过“频道/总线”解耦 发布者不持有订阅者列表,通过事件总线转发;观察者里主题直接持有观察者。

简单记忆:观察者 = 主题维护观察者列表 + 状态变化时 notify 所有观察者 + 一对多


八、常见问题与注意点

8.1 通知顺序

观察者被调用的顺序通常与 attach 顺序一致;若业务对顺序有要求,可以在主题里明确约定或对列表排序。

8.2 观察者里又改主题会导致循环通知吗?

若在某个观察者的 update 里又修改了主题状态,主题再次 notify,可能形成链式甚至循环调用。设计时应避免“通知过程中修改同一主题状态”,或使用“延迟通知”“脏标记”等策略。

8.3 主题销毁时观察者要 detach 吗?

最好在观察者或主题析构时 detach,避免主题仍持有已失效的观察者引用;或在主题 notify 时捕获异常,避免一个观察者出错导致后续观察者不被调用。


九、小结

  • 观察者模式主题维护观察者列表,状态变化时遍历列表通知每个观察者(调用其 update);观察者实现统一接口,收到通知后自行更新。实现一对多、解耦的“订阅-通知”关系。
  • 四个角色:主题(attach/detach/notify)、具体主题(有状态)、观察者(update)、具体观察者(具体反应)。
  • 推模型 / 拉模型:推即 notify 时传新状态;拉即传主题引用,观察者自己取数据。
  • 典型用途:数据与多界面绑定、事件与多处理者、订单/状态变更通知等。

建议先写“天气站 + 多显示屏”,再试“按钮点击”或“订单支付通知”,体会“主题只负责通知、观察者各自响应”,这样对观察者模式会掌握得比较扎实。

发表评论