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 时传新状态;拉即传主题引用,观察者自己取数据。
- 典型用途:数据与多界面绑定、事件与多处理者、订单/状态变更通知等。
建议先写“天气站 + 多显示屏”,再试“按钮点击”或“订单支付通知”,体会“主题只负责通知、观察者各自响应”,这样对观察者模式会掌握得比较扎实。