概述
观察者模式(Observer Pattern)是行为型设计模式的核心成员,其核心目标是定义对象间的一对多依赖关系,当一个对象(被观察者/主题)的状态发生改变时,所有依赖它的对象(观察者)都会收到通知并自动更新。
该模式彻底解决了传统编程中“事件-处理”逻辑耦合紧密、侵入性强的问题,实现了低耦合、非侵入式的通知与更新机制,让主题和观察者各司其职,便于扩展和维护。
核心价值
| 要点 |
详细说明 |
| 解耦彻底 |
主题与观察者通过接口通信,互不依赖具体实现,降低代码耦合度,符合“依赖倒置原则” |
| 扩展性强 |
新增观察者无需修改主题代码,仅需实现观察者接口即可接入,完全遵循“开放-封闭原则” |
| 职责单一 |
主题专注于核心业务逻辑,观察者专注于特定的响应处理,逻辑清晰,便于维护迭代 |
| 通知自动化 |
主题状态变更后,自动触发所有注册观察者的更新操作,无需手动调用,减少重复代码 |
适用场景
| 场景类型 |
具体说明 |
| 多独立后续操作触发 |
一个事件发生后需触发多个独立的后续操作(如购票成功后需记录日志、发送通知、赠送福利等) |
| 后续操作持续扩展 |
后续操作可能持续扩展,需避免修改核心事件的代码(如新增“购票后推送APP消息”无需改动购票逻辑) |
| 核心与后续逻辑分离 |
核心业务与后续处理逻辑需分离(如订单支付与支付后的物流通知、积分发放、发票生成等解耦) |
| 实时响应机制 |
需实现实时响应机制(如状态变更后需立即执行后续操作,而非异步延迟处理) |
注意事项
| 注意点 |
具体说明 |
| 避免循环依赖 |
观察者更新时若触发主题状态再次变更,可能导致循环通知,需谨慎设计逻辑 |
| 区分一致性需求 |
若需“最终一致性”而非“强一致性”(如日志记录可延迟),可考虑消息队列替代,降低同步执行压力 |
| 控制观察者顺序 |
默认按注册顺序执行观察者,若存在依赖顺序的操作,需手动控制注册顺序或引入优先级机制 |
| 避免过度使用 |
若仅需1-2个固定后续操作,直接调用比引入观察者模式更简洁,无需过度设计 |
完整代码
<?php
declare(strict_types = 1);
/**
* 观察者接口:定义观察者的统一规范(接收通知后执行的方法)
* Interface TicketObserver
*/
interface TicketObserver
{
/**
* 接收主题通知后执行的核心方法
* @param object $sender 主题对象(可用于获取主题相关信息)
* @param mixed $args 主题传递的参数(如订单号、购票信息等)
* @return void
*/
public function buyTicketOver($sender, $args): void;
}
/**
* 被观察者接口(主题接口):定义主题的统一规范(注册观察者的方法)
* Interface TicketObserved
*/
interface TicketObserved
{
/**
* 注册观察者(将观察者加入通知列表)
* @param TicketObserver $observer 实现观察者接口的对象
* @return void
*/
public function addObserver(TicketObserver $observer): void;
}
/**
* 具体主题类:购票业务核心逻辑(实现被观察者接口)
* Class BuyTicket
*/
class BuyTicket implements TicketObserved
{
/**
* 存储所有注册的观察者对象
* @var TicketObserver[]
*/
private $observers = [];
/**
* 实现主题接口:添加观察者到通知列表
* @param TicketObserver $observer
* @return void
*/
public function addObserver(TicketObserver $observer): void
{
$this->observers[] = $observer;
}
/**
* 核心业务方法:购票操作
* @param string $ticket 购票信息(如排号、订单号等)
* @return void
*/
public function buyTicketAction(string $ticket): void
{
// 1. 执行核心购票逻辑(如库存校验、订单创建、支付确认等)
echo "核心业务:处理{$ticket}的购票流程,库存锁定成功!<br>";
// 2. 购票成功后,遍历所有观察者并发送通知
$this->notifyObservers($ticket);
}
/**
* 通知所有观察者(私有方法,封装通知逻辑)
* @param string $ticket 购票信息
* @return void
*/
private function notifyObservers(string $ticket): void
{
foreach ($this->observers as $observer) {
// 调用观察者的统一方法,传递主题对象和参数
$observer->buyTicketOver($this, $ticket);
}
}
}
/**
* 具体观察者1:购票成功后发送短信通知
* Class BuyTicketMSN
*/
class BuyTicketMSN implements TicketObserver
{
public function buyTicketOver($sender, $args): void
{
echo date('Y-m-d H:i:s') . " 短信通知:您已成功购买{$args},请凭有效证件入场!<br>";
}
}
/**
* 具体观察者2:购票成功后记录文本日志
* Class BuyTicketLog
*/
class BuyTicketLog implements TicketObserver
{
public function buyTicketOver($sender, $args): void
{
echo date('Y-m-d H:i:s') . " 文本日志:用户成功购买{$args},订单状态已更新为「已支付」<br>";
}
}
/**
* 具体观察者3:购票成功后赠送优惠券
* Class BuyTicketCoupon
*/
class BuyTicketCoupon implements TicketObserver
{
public function buyTicketOver($sender, $args): void
{
echo date('Y-m-d H:i:s') . " 福利赠送:购买{$args}成功,已为您发放10元观影优惠券,有效期7天!<br>";
}
}
// 测试代码
try {
// 1. 创建具体主题(购票业务实例)
$ticketService = new BuyTicket();
// 2. 注册多个观察者(按需添加,无需修改主题代码)
$ticketService->addObserver(new BuyTicketMSN());
$ticketService->addObserver(new BuyTicketLog());
$ticketService->addObserver(new BuyTicketCoupon());
// 3. 触发核心业务(购票操作),自动通知所有观察者
$ticketService->buyTicketAction("7排8号");
} catch (\Exception $e) {
echo "错误:" . $e->getMessage();
}
运行结果
核心业务:处理7排8号的购票流程,库存锁定成功!
2026-01-06 10:30:00 短信通知:您已成功购买7排8号,请凭有效证件入场!
2026-01-06 10:30:00 文本日志:用户成功购买7排8号,订单状态已更新为「已支付」
2026-01-06 10:30:00 福利赠送:购买7排8号成功,已为您发放10元观影优惠券,有效期7天!
核心设计说明
核心结构拆解
// 1. 观察者接口:定义所有观察者的统一方法,保证通知机制一致性
interface TicketObserver
{
public function buyTicketOver($sender, $args): void;
}
// 2. 具体观察者类:实现观察者接口,封装自身响应逻辑(可独立扩展)
class BuyTicketMSN implements TicketObserver
{
public function buyTicketOver($sender, $args): void { /* 短信发送逻辑 */ }
}
// 3. 被观察者接口(主题接口):定义主题的统一规范(注册观察者)
interface TicketObserved
{
public function addObserver(TicketObserver $observer): void;
}
// 4. 具体主题类:实现主题接口,负责核心业务和通知分发
class BuyTicket implements TicketObserved
{
private $observers = []; // 存储观察者列表
public function addObserver(/* 实现 */): void; // 注册观察者
private function notifyObservers(/* 实现 */): void; // 通知所有观察者
public function buyTicketAction(/* 实现 */): void; // 核心业务方法
}
关键优化点(对比传统实现)
// 优化1:引入双接口规范,主题与观察者依赖抽象而非具体,解耦更彻底
// 观察者依赖 TicketObserver 接口,主题依赖 TicketObserved 接口
// 优化2:封装通知逻辑,主题内部通过 notifyObservers 统一分发,避免重复代码
private function notifyObservers(string $ticket): void
{
foreach ($this->observers as $observer) {
$observer->buyTicketOver($this, $ticket);
}
}
// 优化3:明确类型声明(参数类型、返回值),增强代码可读性和类型安全
public function addObserver(TicketObserver $observer): void
public function buyTicketOver($sender, $args): void
// 优化4:遵循开闭原则,新增观察者无需修改主题代码,仅需扩展实现类
// 新增「推送APP消息」观察者,直接实现接口并注册,不改动 BuyTicket 类
// 优化5:主题传递自身引用($this),观察者可按需获取主题状态,灵活性更强
// 例:观察者可通过 $sender 获取订单详情、用户信息等主题相关数据
常见错误规避
// 错误1:主题直接依赖具体观察者,而非观察者接口,导致新增观察者需修改主题
// 解决:定义 TicketObserver 接口,主题仅依赖接口,所有观察者实现该接口
// 错误2:通知逻辑分散在核心业务中,而非封装为独立方法,代码冗余
// 解决:将遍历通知的逻辑封装为 notifyObservers 私有方法,核心业务仅需调用一次
// 错误3:观察者未统一接口,主题通知时需判断类型,耦合度高
// 解决:强制所有观察者实现统一方法(如 buyTicketOver),主题无需判断直接调用
// 错误4:观察者注册后无法移除,导致无用观察者持续接收通知
// 解决:可扩展主题接口,新增 removeObserver 方法,支持观察者动态移除
interface TicketObserved
{
public function addObserver(TicketObserver $observer): void;
public function removeObserver(TicketObserver $observer): void; // 新增移除方法
}
补充说明
扩展场景示例(新增观察者)
若需在购票成功后新增“记录数据库日志”功能,仅需扩展观察者类并注册,无需修改原有代码,完全遵循开闭原则:
// 新增具体观察者:记录数据库日志
class BuyTicketDbLog implements TicketObserver
{
public function buyTicketOver($sender, $args): void
{
// 模拟数据库日志写入逻辑
$logData = [
'ticket' => $args,
'operate_time' => date('Y-m-d H:i:s'),
'log_type' => 'ticket_buy'
];
// $db->insert('operation_log', $logData); // 实际数据库操作
echo date('Y-m-d H:i:s') . " 数据库日志:已记录{$args}的购票操作日志,日志ID:10086<br>";
}
}
// 客户端注册新观察者(无需修改原有代码)
$ticketService->addObserver(new BuyTicketDbLog());
// 触发购票操作,新观察者自动接收通知
$ticketService->buyTicketAction("9排10号");
实际应用场景
- 订单流程:订单支付成功后,触发物流创建、积分发放、发票生成、消息通知等;
- 状态变更:商品库存不足时,通知运营人员、触发补货流程、更新前端库存显示等;
- 日志监控:系统异常时,触发邮件告警、短信通知、日志写入、监控平台上报等;
- 事件订阅:公众号发布文章后,通知所有订阅用户、更新阅读量、推送相关推荐等。
观察者模式与消息队列对比
| 对比维度 |
观察者模式 |
消息队列 |
| 核心目标 |
同步实时通知,观察者即时响应 |
异步解耦,任务后台异步执行 |
| 一致性 |
强一致性(主题变更后立即执行) |
最终一致性(延迟几秒到几分钟) |
| 耦合度 |
低(依赖抽象接口) |
极低(完全独立,通过消息通信) |
| 适用场景 |
需实时响应的后续操作(如短信通知、即时日志) |
可延迟的非核心操作(如统计分析、批量通知) |
| 可靠性 |
观察者异常可能影响主题(需捕获异常) |
消息持久化,失败可重试,不影响生产者 |
| 复杂度 |
简单(仅需双接口+实现类) |
较高(需部署消息队列服务,处理重试、路由) |