观察者模式练习

观察者模式练习——天气站、按钮与订单通知

按《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。

自检问题

  1. 主题为什么要维护观察者列表?
    因为状态变化时要通知所有关心它的对象,主题只负责在变化时遍历列表、调用每个观察者的更新方法,而不需要知道观察者具体类型或业务。

  2. 推模型拉模型有什么区别?
    :notify 时把新状态(如 temperature, humidity)作为参数传给观察者;:notify 时只传主题引用,观察者自己在 update 里向主题取需要的数据。推接口会随“推送内容”变化,拉则主题的 notify 接口更稳定。

  3. 若在某个观察者的 update 里又修改了主题的状态,可能有什么问题?
    主题可能再次 notify,形成链式调用甚至循环;设计上应避免在“被通知”的过程中再去改同一主题的状态,或使用延迟通知等策略。

做完以上三道练习,再对照《观察者模式》文档中的示例,对观察者模式会掌握得比较扎实。建议每道题先自己写一遍,再对照参考答案和验证要点检查。

发表评论