我们常把 23 种经典的设计模式分为三类:创建型、结构型、行为型。前面的文章我们已经讲了了创建型和结构型,我们知道,创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合或组装”问题,从这篇文章开始,我们开始学习行为型设计模式。行为型设计模式主要解决的就是“类或对象之间的交互”问题。行为型设计模式比较多,有 11 个,几乎占了 23 种经典设计模式的一半。它们分别是:观察者模式、模板模式、策略模式、职责链模式、状态模式、迭代器模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式。
这篇文章我们开始学习第一个行为型设计模式,也是在实际的开发中用得比较多的一种模式:观察者模式。

观察者模式(Observer Design Pattern)也被称为发布订阅模式(Publish-Subscribe Design Pattern)。在 GoF 的《设计模式》一书中,它的定义是这样的:

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

翻译成中文就是:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。不过,在实际的项目开发中,这两种对象的称呼是比较灵活的,有各种不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么称呼,只要应用场景符合刚刚给出的定义,都可以看作观察者模式。

观察者模式的结构与实现

结构

  • 抽象主题角色(Subject):Subject类他把所有对观察者对象的引用保存在一个聚集里,每一个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象
  • 抽象观察者角色(Observer):为所有的具体观察者定义一个接口在得到主题的通知时更新自己
  • 具体主题角色(Concrete Subject):将有关状态存入具体观察者对象,在具体主题的内部状态改变时,给所有登记过的观察者发出通知。
  • 具体观察者角色(Concrete Observer):实现抽象观察者角色所需要的更新接口,以便使本身的状态与主题的状态相协调。

代码示例

Subject.php

<?php
namespace DesignPatterns\Observer\example1;
/**
 * Subject类,可翻译为主题或抽象通知者,- -般用-一个抽象类或者- -一个接口实现。它把所有对观察者
 * 对象的引用保存在一一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供一一个接口, 可以增
 * 加和删除观察者对象。
 * Class Subject
 * @package DesignPatterns\Observer\example1
 */
abstract class Subject
{
    private $observers = [];
    //增加观察者
    public function attach(Observer $observer)
    {
        array_push($this->observers,$observer);
    }
    //移除观察者
    public function detach(Observer $observer)
    {
        foreach ($this->observers as $key =>$value){
            if ($observer == $value){
                unset($this->observers[$key]);
            }
        }
    }
    //通知
    public function notify()
    {
        foreach ($this->observers as $observer){
            $observer->update();
        }
    }

}

Observer.php

<?php
namespace DesignPatterns\Observer\example1;
/**
 * Observer类,抽象观察者,为所有的具体观察者定义一一个接口,在得到主题的通知时更新自己。
 * 这个接口叫做更新接口。抽象观察者一-般用一 一个抽象类或者一 一个 接口实现。
 * 更新接口通常包含一个Update()方法,这个方法叫做更新方法。
 * Class Observer
 * @package DesignPatterns\Observer\example1
 */
abstract class Observer
{
    abstract public function update();
}

ConcreteSubject.php

<?php
namespace DesignPatterns\Observer\example1;
/**
 * ConcreteSubject类,叫做具体主题或具体通知者,将有关状态存入具体现察者对象;在具体主题的
 * 内部状态改变时,给所有登记过的观察者发出通知。具体主题角色通常用一个具体子类实现。
 * Class ConcreteSubject
 * @package DesignPatterns\Observer\example1
 */
class ConcreteSubject extends Subject
{
    private $subjectState;

    /**
     * @return mixed
     */
    public function getSubjectState()
    {
        return $this->subjectState;
    }

    /**
     * @param mixed $subjectState
     */
    public function setSubjectState($subjectState): void
    {
        $this->subjectState = $subjectState;
    }
}

ConcreteObserver.php

<?php
namespace DesignPatterns\Observer\example1;
/**
 * ConcreteObserver类,具体观察者,实现抽象观察者角色所要求的更新接口,以便使本身的状态与
 * 主题的状态相协调。具体观察者角色可以保存一个指向具体主题对象的引用。具体观察者角色通常用一个具体子类实现。
 * Class ConcreteObserver
 * @package DesignPatterns\Observer\example1
 */
class ConcreteObserver extends Observer
{
    private $name;
    private $observerState;
    private $subject;

    public function __construct(ConcreteSubject $subject,$name)
    {
        $this->subject = $subject;
        $this->name = $name;
    }

    public function update()
    {
        echo "观察者".$this->name."的新状态是".$this->subject->getSubjectState()."<br>";
    }
}

运行client.php

<?php
namespace DesignPatterns\Observer\example1;
require dirname(__DIR__).'/../vendor/autoload.php';
class Client
{
    public function run()
    {
        $concreteSubject = new ConcreteSubject();
        $concreteSubject->attach(new ConcreteObserver($concreteSubject,"X"));
        $concreteSubject->attach(new ConcreteObserver($concreteSubject,'Y'));
        $z = new ConcreteObserver($concreteSubject,'Z');
        $concreteSubject->attach($z);
        $concreteSubject->detach($z);

        $concreteSubject->setSubjectState("ABC");
        $concreteSubject->notify();

    }
}
$worker = new Client();
$worker->run();

运行结果:

上面的代码算是观察者模式PHP版的模版代码。而在PHP中5.1版本后PHP就内置了很多优秀的特性,其中之一就是提供了一组可以用于观察者设计模式的接口。包括SplObserver接口以及SplSubjectSplObjectStorage接口,利用这些接口,构建PHP观察者模式简直易如反掌。“SPL” 是标准PHP库(Standard PHP Library)的简写,这个库中包括一组解决标准问题的接口和类。
下面是 基于PHP内置观察者模式接口的代码示例:
ConcreteSubject.php

<?php
namespace DesignPatterns\Observer\example2;
/**
 *具体主题
 * Class ConcreteSubject
 * @package DesignPatterns\Observer\example2
 */
class ConcreteSubject implements \SplSubject
{
    protected $subjectState;
    protected $observer;
    public function __construct()
    {
        $this->observer = new \SplObjectStorage();
    }
    //增加观察者
    public function attach(\SplObserver $observer)
    {
        $this->observer->attach($observer);
    }
    //删除观察者
    public function detach(\SplObserver $observer)
    {
        $this->observer->detach($observer);
    }
    //通知
    public function notify()
    {
        foreach ($this->observer as $observer){
            $observer->update($this);
        }
    }

    /**
     * @param mixed $subjectState
     */
    public function setSubjectState($subjectState): void
    {
        $this->subjectState = $subjectState;
    }

    /**
     * @return mixed
     */
    public function getSubjectState()
    {
        return $this->subjectState;
    }
}

ConcreteObserver.php

<?php
namespace DesignPatterns\Observer\example2;
/**
 * 具体观察者
 * Class ConcreteObserver
 * @package DesignPatterns\Observer\example2
 */
class ConcreteObserver implements \SplObserver
{
    private $name;
    private $subject;

    public function __construct(\SplSubject $subject,$name)
    {
        $this->subject = $subject;
        $this->name = $name;
    }

    public function update(\SplSubject $subject)
    {
        echo "观察者".$this->name."的新状态是".$subject->getSubjectState()."<br>";
    }
}

运行Client.php

<?php
namespace DesignPatterns\Observer\example2;
require dirname(__DIR__).'/../vendor/autoload.php';
class Client
{
    public function run()
    {
        $concreteSubject = new ConcreteSubject();
        $concreteSubject->attach(new ConcreteObserver($concreteSubject,'X'));
        $concreteSubject->attach(new ConcreteObserver($concreteSubject,'Y'));

        $z = new ConcreteObserver($concreteSubject,'Z');
        $concreteSubject->attach($z);
        $concreteSubject->detach($z);
        $concreteSubject->setSubjectState("abc");
        $concreteSubject->notify();

    }
}
$worker = new Client();
$worker->run();

运行结果:

观察者模式的原理和代码实现都非常简单,也比较好理解,不需要过多的解释。我们还是通过一个具体的例子来重点讲一下,什么情况下需要用到这种设计模式?或者说,这种设计模式能解决什么问题呢?

示例

假设我们在开发一个 P2P 投资理财系统,用户注册成功之后,我们会给用户发放投资体验金。代码实现大致是下面这个样子的。

class User
{
    public function register($phone)
    {
        //省略输入参数的校验代码
        echo "用户注册成功,用户账号".$phone;//简化注册
        echo "发放投资体验金500";//简化发放投资体验金
    }
}

虽然注册接口做了两件事情,注册和发放体验金,违反单一职责原则,但是,如果没有扩展和修改的需求,现在的代码实现是可以接受的。如果非得用观察者模式,就需要引入更多的类和更加复杂的代码结构,反倒是一种过度设计。
相反,如果需求频繁变动,比如,用户注册成功之后,不再发放体验金,而是改为发放优惠券,并且还要给用户发送一封“欢迎注册成功”的站内信。这种情况下,我们就需要频繁地修改 register() 函数中的代码,违反开闭原则。而且,如果注册成功之后需要执行的后续操作越来越多,那 register() 函数的逻辑会变得越来越复杂,也就影响到代码的可读性和可维护性。
这个时候,观察者模式就能派上用场了。利用观察者模式,我对上面的代码进行了重构。重构之后的代码如下所示:

interface RegObserver
{
    public function handleRegSuccess($phone);
}


class RegPromotionObserver implements RegObserver
{

    public function handleRegSuccess($phone)
    {
        echo "用户 ".$phone.",你的投资体验金500元已到账<br>";
    }
}


class RegNotificationObserver implements RegObserver
{

    public function handleRegSuccess($phone)
    {
        echo "用户 ".$phone.",welcome <br>";
    }
}

class User
{
    private $regObservers=[];

    /**
     * @param mixed $regObservers
     */
    public function setRegObservers(RegObserver$regObservers)
    {
        $this->regObservers[] = $regObservers;
    }
    public function register($phone)
    {
        //省略输入参数的校验代码
        //省略注册
        foreach ($this->regObservers as $regObserver){
            $regObserver->handleRegSuccess($phone);
        }
    }
}

//clientCode
$user = new User();
$user->setRegObservers(new RegPromotionObserver());
$user->setRegObservers(new RegNotificationObserver());
$user->register('12345678901');

运行结果:

当我们需要添加新的观察者的时候,比如,用户注册成功之后,推送用户注册信息给大数据征信系统,基于观察者模式的代码实现,UserController 类的 register() 函数完全不需要修改,只需要再添加一个实现了RegObserver 接口的类,并且通过 setRegObservers() 函数将它注册到 UserController 类中即可。
不过,你可能会说,当我们把发送体验金替换为发送优惠券的时候,需要修改 RegPromotionObserver 类中 handleRegSuccess() 函数的代码,这还是违反开闭原则呀?你说得没错,不过,相对于 register() 函数来说,handleRegSuccess() 函数的逻辑要简单很多,修改更不容易出错,引入 bug 的风险更低。

前面我们已经学习了很多设计模式,不知道你有没有发现,实际上,设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦,具体到观察者模式,它是将观察者和被观察者代码解耦。借助设计模式,我们利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚松耦合等特性,以此来控制和应对代码的复杂性,提高代码的可扩展性。

总结

观察者模式,定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
观察者模式所做的工作其实就是在接触耦合。让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响另一边的变化。

参考文章:设计模式之美-观察者模式(上):详解各种应用场景下观察者模式的不同实现方式
github示例:https://github.com/yangpanyao/design-patterns/tree/master/Observer

Last modification:June 29th, 2020 at 10:24 pm