PHP 观察者模式

概述

观察者模式(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号");

实际应用场景

  • 订单流程:订单支付成功后,触发物流创建、积分发放、发票生成、消息通知等;
  • 状态变更:商品库存不足时,通知运营人员、触发补货流程、更新前端库存显示等;
  • 日志监控:系统异常时,触发邮件告警、短信通知、日志写入、监控平台上报等;
  • 事件订阅:公众号发布文章后,通知所有订阅用户、更新阅读量、推送相关推荐等。

观察者模式与消息队列对比

对比维度 观察者模式 消息队列
核心目标 同步实时通知,观察者即时响应 异步解耦,任务后台异步执行
一致性 强一致性(主题变更后立即执行) 最终一致性(延迟几秒到几分钟)
耦合度 低(依赖抽象接口) 极低(完全独立,通过消息通信)
适用场景 需实时响应的后续操作(如短信通知、即时日志) 可延迟的非核心操作(如统计分析、批量通知)
可靠性 观察者异常可能影响主题(需捕获异常) 消息持久化,失败可重试,不影响生产者
复杂度 简单(仅需双接口+实现类) 较高(需部署消息队列服务,处理重试、路由)