观察者模式练习——天气站、按钮与订单通知
按《Python 设计模式——观察者模式》的建议,通过三道练习巩固:① 天气站与多个显示屏(主题 + 观察者);② 按钮点击与多个监听器;③ 订单支付成功通知多个处理者。每步都有完整可运行代码和验证要点。
练习一:天气站与多个显示屏
目的
体会主题维护观察者列表、状态变化时 notify 所有观察者;主题不依赖具体观察者类型,只依赖“观察者接口”;增加新显示屏只需 new 并 attach,无需改主题代码。
要求
- 实现 Subject 接口:attach(observer)、detach(observer)、notify()。
- 实现 Observer 接口:update(temperature, humidity)(推模型)。
- 实现 WeatherStation(具体主题):有温度、湿度;set_measurements(t, h) 时更新内部状态并 notify()。
- 实现 DisplayScreen(具体观察者):有名字;update(t, h) 时打印一条“[名字] 温度=xx 湿度=xx”。
- 客户代码:创建一个 WeatherStation,attach 两个 DisplayScreen,调用 set_measurements(25.5, 60),验证两个屏都打印了更新;再 detach 一个屏,再次 set_measurements,验证只有仍 attach 的屏打印。
参考答案
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
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()
class DisplayScreen(Observer):
def __init__(self, name: str):
self.name = name
def update(self, temperature: float, humidity: float):
print(f" [{self.name}] 温度={temperature}, 湿度={humidity}")
# 使用
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
station.detach(screen2)
station.set_measurements(30.0, 70)
# 只有 [屏1] 打印
验证要点
- 第一次 set_measurements 后,两个屏都打印一次。
- detach(screen2) 后再次 set_measurements,只有屏1打印,屏2 不再收到通知。
- 确认:WeatherStation 只依赖 Observer 接口,不依赖 DisplayScreen 具体类。
练习二:按钮点击与多个监听器
目的
把“按钮”当作主题,“点击”当作状态变化;多个监听器(观察者)订阅点击事件,点击时主题 notify,每个监听器执行自己的逻辑(写日志、弹提示等)。
要求
- 实现 Button(主题):有 attach(listener),监听器需有 on_click() 方法;有 click(),被调用时遍历所有监听器并调用 listener.on_click()。
- 实现两个监听器:LogListener(on_click 时打印“[日志] 按钮被点击”)、AlertListener(on_click 时打印“[提示] 您点击了按钮”)。
- 客户代码:创建一个 Button,attach 两个监听器,调用 button.click(),验证两行输出;再 attach 第三个监听器(自拟),再次 click,验证三个监听器都被调用。
参考答案
class Button:
def __init__(self):
self._listeners = []
def attach(self, listener):
"""listener 需有 on_click() 方法"""
self._listeners.append(listener)
def click(self):
for L in self._listeners:
L.on_click()
class LogListener:
def on_click(self):
print(" [日志] 按钮被点击")
class AlertListener:
def on_click(self):
print(" [提示] 您点击了按钮")
class CounterListener:
def __init__(self):
self.count = 0
def on_click(self):
self.count += 1
print(f" [计数] 第 {self.count} 次点击")
# 使用
button = Button()
button.attach(LogListener())
button.attach(AlertListener())
button.click()
# [日志] 按钮被点击
# [提示] 您点击了按钮
button.attach(CounterListener())
button.click()
# 三个监听器都会执行
验证要点
- 第一次 click 有 两行输出(日志、提示)。
- 增加 CounterListener 后第二次 click 有 三行输出,且计数正确。
- 确认:Button 不依赖具体监听器类,只要带 on_click() 即可。
练习三:订单支付成功通知多个处理者
目的
订单(主题)在“支付成功”时通知多个观察者:库存扣减、发邮件、更新统计等。订单不直接依赖库存、邮件、统计模块,只维护观察者列表,支付时 notify;新增一种通知方式只需加一个观察者并 attach。
要求
- 定义观察者接口 OrderObserver,有方法 on_order_paid(order_id, amount)。
- 实现 Order(主题):有 order_id、amount;有 attach(observer);有 pay(amount),pay 时把 amount 存下来并遍历观察者调用 on_order_paid(self.order_id, self.amount)。
- 实现两个具体观察者:InventoryObserver(on_order_paid 时打印“[库存] 扣减订单 xxx 的库存”)、EmailObserver(on_order_paid 时打印“[邮件] 发送订单 xxx 支付成功邮件”)。
- 客户代码:创建订单,attach 两个观察者,调用 order.pay(99.0),验证两行输出。
参考答案
from abc import ABC, abstractmethod
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._observers = []
def attach(self, observer: OrderObserver):
self._observers.append(observer)
def pay(self, amount: float):
self.amount = amount
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.pay(99.0) 后,库存和邮件两行都打印,且 order_id、amount 正确。
- 确认:Order 只依赖 OrderObserver 接口;若要加“短信通知”,只需新写一个实现 OrderObserver 的类并 attach,无需改 Order。
三步汇总与自检
| 练习 | 重点 | 关键点 |
|---|---|---|
| 一 | 天气站 + 显示屏 | 主题 attach/detach/notify;观察者 update(t, h);detach 后不再收到通知。 |
| 二 | 按钮 + 监听器 | 主题 click 时遍历监听器调用 on_click;可随时 attach 新监听器。 |
| 三 | 订单支付通知 | 主题 pay 时 notify,传 order_id 和 amount;观察者实现 on_order_paid。 |
自检问题
-
主题为什么要维护观察者列表?
因为状态变化时要通知所有关心它的对象,主题只负责在变化时遍历列表、调用每个观察者的更新方法,而不需要知道观察者具体类型或业务。 -
推模型和拉模型有什么区别?
推:notify 时把新状态(如 temperature, humidity)作为参数传给观察者;拉:notify 时只传主题引用,观察者自己在 update 里向主题取需要的数据。推接口会随“推送内容”变化,拉则主题的 notify 接口更稳定。 -
若在某个观察者的 update 里又修改了主题的状态,可能有什么问题?
主题可能再次 notify,形成链式调用甚至循环;设计上应避免在“被通知”的过程中再去改同一主题的状态,或使用延迟通知等策略。
做完以上三道练习,再对照《观察者模式》文档中的示例,对观察者模式会掌握得比较扎实。建议每道题先自己写一遍,再对照参考答案和验证要点检查。