Skip to content

观察者模式(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 - 20

2.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;
}

七、全文总结

观察者模式的核心是 定义对象间一对多依赖,状态变化时自动通知所有观察者

核心要点

  1. 主题维护观察者列表,状态变化时通知
  2. 推模型简单直接,拉模型灵活
  3. 注意内存泄漏(及时取消订阅)
  4. 避免级联更新导致的循环
  5. 是事件系统、MVVM、响应式编程的基础