观察者模式(Observer)
前言
观察者模式定义了对象间一对多的依赖关系,当一个对象状态改变时,所有依赖者都会收到通知。它是事件驱动编程、MVVM 架构、响应式编程的核心。本文将详细讲解观察者模式的核心原理、推拉模型及实际应用。
一、核心概念
1.1 定义
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
1.2 核心角色
| 角色 | 说明 |
|---|---|
| 主题(Subject) | 维护观察者列表,状态变化时通知 |
| 具体主题(ConcreteSubject) | 实现通知逻辑,存储状态 |
| 观察者(Observer) | 定义更新接口 |
| 具体观察者(ConcreteObserver) | 实现更新逻辑 |
1.3 推模型 vs 拉模型
| 模型 | 说明 | 优缺点 |
|---|---|---|
| 推模型 | 主题将数据推给观察者 | 实现简单,但可能传多余数据 |
| 拉模型 | 观察者从主题拉取需要的数据 | 灵活,但耦合度稍高 |
二、代码实现
2.1 基础实现
php
<?php
// 观察者接口
interface Observer
{
public function update($data): void;
}
// 主题接口
interface Subject
{
public function attach(Observer $observer): void;
public function detach(Observer $observer): void;
public function notify(): void;
}
// 具体主题
class ConcreteSubject implements Subject
{
/** @var Observer[] */
private array $observers = [];
private int $state = 0;
public function getState(): int
{
return $this->state;
}
public function setState(int $state): void
{
$this->state = $state;
$this->notify(); // 状态变化时自动通知
}
public function attach(Observer $observer): void
{
if (!in_array($observer, $this->observers, true)) {
$this->observers[] = $observer;
}
}
public function detach(Observer $observer): void
{
$index = array_search($observer, $this->observers, true);
if ($index !== false) {
array_splice($this->observers, (int)$index, 1);
}
}
public function notify(): void
{
foreach ($this->observers as $observer) {
$observer->update($this->state);
}
}
}
// 具体观察者
class ConcreteObserverA implements Observer
{
public function update($data): void
{
echo "ObserverA: received data - {$data}" . PHP_EOL;
}
}
class ConcreteObserverB implements Observer
{
public function update($data): void
{
echo "ObserverB: received data - {$data}" . PHP_EOL;
}
}
// 使用
$subject = new ConcreteSubject();
$observerA = new ConcreteObserverA();
$observerB = new ConcreteObserverB();
$subject->attach($observerA);
$subject->attach($observerB);
$subject->setState(10); // 通知所有观察者
// ObserverA: received data - 10
// ObserverB: received data - 10
$subject->detach($observerA);
$subject->setState(20); // 只有 B 收到通知
// ObserverB: received data - 202.2 实际案例:事件系统
php
<?php
// 事件处理器类型:callable
class EventEmitter
{
/** @var array<string, callable[]> */
private array $events = [];
// 订阅事件
public function on(string $event, callable $handler): void
{
if (!isset($this->events[$event])) {
$this->events[$event] = [];
}
$this->events[$event][] = $handler;
}
// 取消订阅
public function off(string $event, callable $handler): void
{
if (!isset($this->events[$event])) {
return;
}
$index = array_search($handler, $this->events[$event], true);
if ($index !== false) {
array_splice($this->events[$event], (int)$index, 1);
}
}
// 只订阅一次
public function once(string $event, callable $handler): void
{
$wrapper = function ($data) use ($event, $handler, &$wrapper) {
$handler($data);
$this->off($event, $wrapper);
};
$this->on($event, $wrapper);
}
// 触发事件
public function emit(string $event, $data = null): void
{
if (!isset($this->events[$event])) {
return;
}
foreach ($this->events[$event] as $handler) {
$handler($data);
}
}
}
// 使用
$emitter = new EventEmitter();
// 订阅
$emitter->on("user:login", function ($user) {
echo "Welcome, {$user['name']}!" . PHP_EOL;
});
$emitter->on("user:login", function ($user) {
echo "Login time: " . date('c') . PHP_EOL;
});
// 触发
$emitter->emit("user:login", ['name' => 'Flynn']);
// Welcome, Flynn!
// Login time: 2026-07-03T...
// 只触发一次
$emitter->once("system:shutdown", function () {
echo "System shutting down..." . PHP_EOL;
});
$emitter->emit("system:shutdown"); // 触发
$emitter->emit("system:shutdown"); // 不触发2.3 实际案例:股票价格监控
php
<?php
// 股票主题
class Stock implements Subject
{
/** @var Observer[] */
private array $observers = [];
private float $price;
public function __construct(
private string $symbol,
float $initialPrice
) {
$this->price = $initialPrice;
}
public function getPrice(): float
{
return $this->price;
}
public function setPrice(float $newPrice): void
{
$oldPrice = $this->price;
$this->price = $newPrice;
echo "{$this->symbol}: \${$oldPrice} → \${$newPrice}" . PHP_EOL;
$this->notify();
}
public function attach(Observer $observer): void
{
$this->observers[] = $observer;
}
public function detach(Observer $observer): void
{
$this->observers = array_values(array_filter(
$this->observers,
fn($o) => $o !== $observer
));
}
public function notify(): void
{
$data = ['symbol' => $this->symbol, 'price' => $this->price];
foreach ($this->observers as $o) {
$o->update($data);
}
}
}
// 邮件通知观察者
class EmailNotifier implements Observer
{
public function update($data): void
{
echo "📧 Email: {$data['symbol']} price changed to \${$data['price']}" . PHP_EOL;
}
}
// 日志记录观察者
class PriceLogger implements Observer
{
public function update($data): void
{
echo "📝 Log: {$data['symbol']} = \${$data['price']}" . PHP_EOL;
}
}
// 预警观察者
class PriceAlert implements Observer
{
public function __construct(private float $threshold) {}
public function update($data): void
{
if ($data['price'] > $this->threshold) {
echo "🚨 Alert: {$data['symbol']} exceeded \${$this->threshold}!" . PHP_EOL;
}
}
}
// 使用
$stock = new Stock("AAPL", 150);
$stock->attach(new EmailNotifier());
$stock->attach(new PriceLogger());
$stock->attach(new PriceAlert(160));
$stock->setPrice(155);
$stock->setPrice(165); // 触发预警三、适用场景
| 场景 | 说明 |
|---|---|
| 事件系统 | DOM 事件、自定义事件 |
| MVVM 架构 | 数据绑定与视图更新 |
| 消息队列 | 发布-订阅模型 |
| 股票/汇率监控 | 数据变化通知 |
| 状态管理 | Redux/Vuex 状态变化 |
| 实时通信 | WebSocket 消息推送 |
四、优缺点分析
| 优点 | 缺点 |
|---|---|
| 解耦主题和观察者 | 可能产生大量通知 |
| 支持广播通信 | 观察者可能不知道来源 |
| 符合开闭原则 | 可能导致内存泄漏 |
| 运行时动态订阅/取消 | 更新顺序不可控 |
| 符合松耦合设计 | 可能引发级联更新 |
五、常见踩坑与问题排查
5.1 内存泄漏
php
<?php
// 问题:忘记取消订阅导致内存泄漏
class BadComponent
{
public function __construct(EventEmitter $emitter)
{
$emitter->on("data", [$this, "handleData"]);
// 组件销毁时没有 off
}
}
// 解决:在销毁时取消订阅
class GoodComponent
{
private $handler;
public function __construct(EventEmitter $emitter)
{
$this->handler = function ($data) {
$this->handleData($data);
};
$emitter->on("data", $this->handler);
}
public function destroy(EventEmitter $emitter): void
{
$emitter->off("data", $this->handler);
}
}5.2 级联更新导致循环
php
<?php
// 问题:A 通知 B,B 通知 A,无限循环
// 解决:引入更新标记,避免循环通知
class SafeSubject implements Subject
{
private bool $isNotifying = false;
public function notify(): void
{
if ($this->isNotifying) return; // 防止重入
$this->isNotifying = true;
foreach ($this->observers as $o) {
$o->update($this->state);
}
$this->isNotifying = false;
}
}5.3 闭包导致无法取消订阅
php
<?php
// 问题:每次 on 传入新的闭包,无法 off
$emitter->on("data", function ($data) { $this->handle($data); });
$emitter->off("data", function ($data) { $this->handle($data); }); // 无法匹配!
// 解决:保存闭包引用
$handler = function ($data) { $this->handle($data); };
$emitter->on("data", $handler);
$emitter->off("data", $handler);六、优化方案与进阶
6.1 RxPHP / ReactPHP 响应式编程
php
<?php
// 使用 RxPHP 库(需 composer require react/promise rx/rx-php)
use Rx\Subject\Subject;
$subject = new Subject();
$subject->subscribe(
function ($value) { echo "Observer A: {$value}" . PHP_EOL; }
);
$subject->subscribe(
function ($value) { echo "Observer B: {$value}" . PHP_EOL; }
);
$subject->onNext(1);
$subject->onNext(2);6.2 推拉结合
php
<?php
interface HybridObserver
{
// 推模型:主题推送数据
public function updatePush($data): void;
// 拉模型:观察者主动拉取
public function updatePull(Subject $subject): void;
}七、全文总结
观察者模式的核心是 定义对象间一对多依赖,状态变化时自动通知所有观察者。
核心要点:
- 主题维护观察者列表,状态变化时通知
- 推模型简单直接,拉模型灵活
- 注意内存泄漏(及时取消订阅)
- 避免级联更新导致的循环
- 是事件系统、MVVM、响应式编程的基础
