我们常把 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接口以及SplSubject和SplObjectStorage接口,利用这些接口,构建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
啊啊啊哈