PHP 设计模式系列 —— 观察者模式(Observer)

1、模式定义

观察者模式有时也被称作发布/订阅模式,该模式用于为对象实现发布/订阅功能:一旦主体对象状态发生改变,与之关联的观察者对象会收到通知,并进行相应操作。

将一个系统分割成一个一些类相互协作的类有一个不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。观察者就是解决这类的耦合关系的。

消息队列系统、事件都使用了观察者模式。

PHP 为观察者模式定义了两个接口:SplSubject 和 SplObserver。SplSubject 可以看做主体对象的抽象,SplObserver 可以看做观察者对象的抽象,要实现观察者模式,只需让主体对象实现 SplSubject ,观察者对象实现 SplObserver,并实现相应方法即可。

2、UML类图

Observer-Design-Pattern-Uml

3、示例代码

User.php

<?php

namespace DesignPatterns\Behavioral\Observer;

/**
 * 观察者模式 : 被观察对象 (主体对象)
 *
 * 主体对象维护观察者列表并发送通知
 *
 */
class User implements \SplSubject
{
    /**
     * user data
     *
     * @var array
     */
    protected $data = array();

    /**
     * observers
     *
     * @var \SplObjectStorage
     */
    protected $observers;
    
    public function __construct()
    {
        $this->observers = new \SplObjectStorage();
    }

    /**
     * 附加观察者
     *
     * @param \SplObserver $observer
     *
     * @return void
     */
    public function attach(\SplObserver $observer)
    {
        $this->observers->attach($observer);
    }

    /**
     * 取消观察者
     *
     * @param \SplObserver $observer
     *
     * @return void
     */
    public function detach(\SplObserver $observer)
    {
        $this->observers->detach($observer);
    }

    /**
     * 通知观察者方法
     *
     * @return void
     */
    public function notify()
    {
        /** @var \SplObserver $observer */
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    /**
     *
     * @param string $name
     * @param mixed  $value
     *
     * @return void
     */
    public function __set($name, $value)
    {
        $this->data[$name] = $value;

        // 通知观察者用户被改变
        $this->notify();
    }
}

UserObserver.php

<?php

namespace DesignPatterns\Behavioral\Observer;

/**
 * UserObserver 类(观察者对象)
 */
class UserObserver implements \SplObserver
{
    /**
     * 观察者要实现的唯一方法
     * 也是被 Subject 调用的方法
     *
     * @param \SplSubject $subject
     */
    public function update(\SplSubject $subject)
    {
        echo get_class($subject) . ' has been updated';
    }
}

4、测试代码

Tests/ObserverTest.php

<?php

namespace DesignPatterns\Behavioral\Observer\Tests;

use DesignPatterns\Behavioral\Observer\UserObserver;
use DesignPatterns\Behavioral\Observer\User;

/**
 * ObserverTest 测试观察者模式
 */
class ObserverTest extends \PHPUnit_Framework_TestCase
{

    protected $observer;

    protected function setUp()
    {
        $this->observer = new UserObserver();
    }

    /**
     * 测试通知
     */
    public function testNotify()
    {
        $this->expectOutputString('DesignPatterns\Behavioral\Observer\User has been updated');
        $subject = new User();

        $subject->attach($this->observer);
        $subject->property = 123;
    }

    /**
     * 测试订阅
     */
    public function testAttachDetach()
    {
        $subject = new User();
        $reflection = new \ReflectionProperty($subject, 'observers');

        $reflection->setAccessible(true);
        /** @var \SplObjectStorage $observers */
        $observers = $reflection->getValue($subject);

        $this->assertInstanceOf('SplObjectStorage', $observers);
        $this->assertFalse($observers->contains($this->observer));

        $subject->attach($this->observer);
        $this->assertTrue($observers->contains($this->observer));

        $subject->detach($this->observer);
        $this->assertFalse($observers->contains($this->observer));
    }

    /**
     * 测试 update() 调用
     */
    public function testUpdateCalling()
    {
        $subject = new User();
        $observer = $this->getMock('SplObserver');
        $subject->attach($observer);

        $observer->expects($this->once())
            ->method('update')
            ->with($subject);

        $subject->notify();
    }
}

5、总结

观察者模式解除了主体和具体观察者的耦合,让耦合的双方都依赖于抽象,而不是依赖具体。从而使得各自的变化都不会影响另一边的变化。

学院君 has written 1244 articles

Laravel学院院长,终身学习者

积分:168212 等级:P12 职业:手艺人 城市:杭州

13 条回复

  1. 学院君 学院君 says:
    @ SparkLee

    嗯 感谢你的建议 后面有时间我会把设计模式这块整体优化下 毕竟知识本身也是在不断更新的 过一段时间可能之前的描述就不那么适用了

  2. SparkLee SparkLee says:
    @ 学院君

    如吧,"本质错误"我收回。 但是"观察者模式有时也被称作发布/订阅模式"中的"也被称作"这个描述实在是太不严谨了,或者这么说也行,但至少在后文,应该也要提一下两者的差异,不然会让人产生一种"两者等同"的认知,会有点误导。

  3. 学院君 学院君 says:
    @ SparkLee

    这句话来自《大话设计模式》一书,我们可以把 subject 看作发布者,observer 看作订阅者,反过来,可以把发布订阅模式看作松耦合的观察者模式实现,也没什么错,在 GoF 的《设计模式》一书中,对观察者模式的定义只是一个基本概念,没有对这些细节做定义,在 23 种设计模式中没有发布订阅模式,发布/订阅模式其实是从消息系统中作为架构模式迁移而来,在 Windows 系统中也将这两个模式视作同义词,你可以参考维基百科https://en.wikipedia.org/wiki/Observer_pattern#Coupling_and_typical_pub-sub_implementations中的介绍,从宽松的角度来说,把它们看作一个模式没什么问题,发布/订阅模式只是观察者模式的一种松耦合实现,从严格角度来说,也可以把它们看作不同的模式,比如 《JavaScript 设计模式》一书中就做了这样的区分:https://www.oreilly.com/library/view/learning-javascript-design/9781449334840/ch09s05.html,看你的理解角度了,比如微服务和服务化,有些人认为它们就是一个东西,有些人则要严格区分它们。

    「本质错误」这样的措辞有点夸张了。

  4. SparkLee SparkLee says:

    开篇的第一句话("观察者模式有时也被称作发布/订阅模式")是有本质错误的。

    观察者模式中Subject和Observer之间是存在耦合的;

    但是发布/订阅模式中的Publisher和Subscriber之间是解耦合的,发布者与订阅者通过消息中间件进行解耦合。

  5. 徐卡丘 徐卡丘 says:
    我想$this->observers->attach($observer)这样写是指调用了SplObjectStorage对象的attach方法。添加对象到Storage中
  6. yorick yorick says:
    @ Chris $this->observers = new \SplObjectStorage(); observers 是Spl对象 要看仔细点
  7. 小龙 小龙 says:
    @ 涩 为什么有了构造函数$this->observers = new SplObjectStorage() 就可以$this->observers->attach($observer);这样写呢?这不还是调用了自己吗?还有,不知道这种设计模式怎么使用到具体场景。做测试,我知道原理,也知道怎么测试。但那是在一个文件里面测试。正常的话,应该是user类发出一个通知,观察者能够正常接收才对。可是现在的测试是,自己设置的user类做出动作,然后自己的内容做出了改变。我觉得这样没有意义了啊

登录后才能进行评论,立即登录?