PHP-观察者(Observer) 设计模式

时间:2019-02-14
本文章向大家介绍PHP-观察者(Observer) 设计模式,主要包括PHP-观察者(Observer) 设计模式使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

说到PHP的设计模式大家往往会想到单例、工厂等设计模式,我们今天来说说我感觉比较好的一种设计模式——观察者模式。

说到观察者模式大家可能不太明白,什么是观察者模式。所谓观察者模式,我们可以通俗的理解为你玩在玩炉石传说的时候,好友在你的对局中观战。你的好友就是观察者,你就是被观察者。当你胜利或者失败观察者都会在视觉上收到通知。哈哈~大家应该懂了吧。

那么这个观察者模式有什么用呢?他可以实现当一些数据发上改变时通知你的观察者。利用的场景有哪些呢?(我什么时候能改一下自问自答的习惯…)我们说一下,比如你想实现一个在站内信提醒功能、创建用户短信提醒功能、数据超出一定数量报警功能等等的一下你觉得可以使用在上面的功能。那么我们接下来说说观察者模式的实现方式。

我们利用PHP自带SPL快速的实现观察者(Observer)设计模式。说道这里大家问了,何为SPL。我们来解释一下,官方是这样解释的:SPL是用于解决典型问题(standard problems)的一组接口与类的集合。我在补充一下,他是 PHP 5 在面向对象上能力提升的真实写照,它由一系列内置的类、接口和函数构成。SPL 通过加入集合,迭代器,新的异常类型,文件和数据处理类等提升了 PHP 语言的生产力。它还提供了一些十分有用的特性,如本文要介绍的内置 Observer 设计模式(所以我们需要在PHP5.2及以上版本实现)。我们就使用 SPL 提供的 SplSubject和 SplObserver接口以及 SplObjectStorage类,快速实现 Observer 设计模式。

下面我们介绍一下SplSubject 和 SplObserver 接口。

Observer 设计模式定义了对象间的一种一对多的依赖关系,当被观察的对象发生改变时,所有依赖于它的对象都会得到通知并被自动更新,而且被观察的对象和观察者之间是松耦合的。在该模式中,有目标(Subject)和观察者(Observer)两种角色。目标角色是被观察的对象,持有并控制着某种状态,可以被任意多个观察者作为观察的目标,SPL 中使用 SplSubject接口规范了该角色的行为。

SplSubject 接口中的方法:

abstract public void attach ( SplObserver $observer ) 添加(注册)一个观察者

abstract public void detach ( SplObserver $observer ) 删除一个观察者

abstract public void notify ( void ) 当状态发生改变时,通知所有观察者

观察者角色是在目标发生改变时,需要得到通知的对象。SPL 中用 SplObserver接口规范了该角色的行为:

SplObserver 中的方法:

abstract public void update ( SplSubject $subject ) 在目标发生改变时接收目标发送的通知;当关注的目标调用其 notify()时被调用

该设计模式的核心思想是,SplSubject对象会在其状态改变时调用 notify()方法,一旦这个方法被调用,任何先前通过 attach()方法注册上来的 SplObserver对象都会以调用其 update()方法的方式被更新。

这里我们不得不说说SplObjectStorage 类,这个类的实例很像一个数组,但是它所存放的对象都是唯一的。这个特点就为快速实现 Observer 设计模式贡献了不少力量,因为我们不希望同一个观察者被注册多次。该类的另一个特点是,可以直接从中删除指定的对象,而不需要遍历或搜索整个集合(这就是我们可以快速实现观察者模式的重点)。

说了这么多,我们来句一个例子来实现我们上面说的,这样也可以更生动形象的体会这个设计模式。接下来我就来实现一个我们小的时候都经历过的真是事件,那就是家长监督放假在家的你的各种行为。这个观察者模式就相当于你家中的摄像头。嘿嘿()

首先我们被观察的对象叫小明(为什么我总出现在例子中…)

class Xiaoming implements SplSubject
{
    //属性0为没有做 1位正在做
    private $Sleep = 1;
    private $Play = 0;
    private $Eat = 0;
    private $Study = 0;
    private $Do = [];
     
    //小明的观察者
    private $ObServers;
     
    public function __construct()
    {
        $this->ObServers = new SplObjectStorage;
    }
     
    public function Eat($Prop)
    {
        $this->Eat = $Prop;
        $this->notify();
    }
     
    public function Play($Prop)
    {
        $this->Play = $Prop;
        $this->notify();
    }
     
    public function Study($Prop)
    {
        $this->Study = $Prop;
        $this->notify();
    } 
     
    public function Sleep($Prop)
    {
        $this->Sleep = $Prop;
        $this->notify();
    }
     
    //增加观察者
    public function attach(SplObserver $observer)
    {
        if(is_object($observer) && !$this->ObServers->contains($observer)){
            $this->ObServers->attach($observer);
        }
        return true;
    }
     
    //删除观察者
    public function detach(SplObserver $observer)
    {
        if(!is_object($observer) || !$this->ObServers->contains($observer)){
            return false;
        }
        $this->ObServers->detach($observer);
        return true;
    }
     
    //当状态改变时,通知观察者
    public function notify()
    {
        $this->Do = [];
        $param['Sleep'] = $this->Sleep;
        $param['Play'] = $this->Play;
        $param['Eat'] = $this->Eat;
        $param['Study'] = $this->Study;
        foreach ($param as $k => $v) {
            if($v == 1){
                $this->Do[] = $k; 
            }
            if(count($this->Do) > 1){
                throw new Exception("不要一心二用,小明在".$this->Do[0].',不能'.$this->Do[1]);
            }
        }
         
        foreach ($this->ObServers as $observer) {
            $observer->update($this);
        }
    }
     
    public function __get($Prop)
    {
        switch ($Prop) {
            case 'Sleep':
            return $this->Sleep;
            break;
            case 'Play':
            return $this->Play;
            break;
            case 'Eat':
            return $this->Eat;
            break;
            case 'Study':
            return $this->Study;
            break;
            default:
            throw new Exception($Prop . 'cannotberead');
        }
    }
     
    public function __set($Prop, $Val)
    {
        throw new Exception($Prop . 'cannotbeset');
    }
}

这里我们创建一个叫小明的被观察者,他被观察的动作有睡觉、吃饭、玩和学习。我们这里用“拉”的方式获取小明的实时动态,我们也可以用“推”的方式获取。如果用“推”的方式获取那么我们就不需要__get、__set方法了。我们需要修改notify方法

public function notify()
{
    $this->Do = [];
    $param['Sleep'] = $this->Sleep;
    $param['Play'] = $this->Play;
    $param['Eat'] = $this->Eat;
    $param['Study'] = $this->Study;
    foreach ($param as $k => $v) {
        if($v == 1){
            $this->Do[] = $k; 
        }
        if(count($this->Do) > 1){
            throw new Exception("不要一心二用,小明在".$this->Do[0].',不能'.$this->Do[1]);
        }
    }
     
    foreach ($this->ObServers as $observer) {
        $observer->update($this,$param);//改动在这里,添加一个参数
    }
}

不过下面观察者也需要改动,稍后再说,我们先按照“拉”的方式继续写代码。

class Grandpa implements SplObserver
{
    private $SubjectState;
    public function update(SplSubject $subject)
    {
        switch ($subject->Sleep) {
            case 0:
            if($this->SubjectState !== 1){
                echo '爷爷观察小明起床了<br/>';
                $this->SubjectState = 1;
            }
            break;
            case 1:
            if(is_null($this->SubjectState) || $this->SubjectState == 1){
                echo '爷爷观察小明在睡觉<br/>';
            }
            $this->SubjectState = 0;
            break;
            default:
            throw new Exception('UnexpectederrorincarStateObserver::update()');
            break;
        }
    }
}
 
class Father implements SplObserver
{
    public function update(SplSubject $subject)
    {
        if ($subject->Play == 1) {
            echo '爸爸观察小明在玩游戏<br/>';
        }
    }
}
 
class Mother implements SplObserver
{
    public function update(SplSubject $subject)
    {
        if ($subject->Study == 1) {
            echo '妈妈观察小明在学习<br/>';
        }
    }
}
 
class Sister implements SplObserver
{
    public function update(SplSubject $subject)
    {
        if ($subject->Eat == 1) {
            echo '妹妹观察小明在吃零食<br/>';
        }
    }
}

这里我们创建了4个观察者(小明真的是惨),他们分别是爷爷->睡觉、爸爸->玩、妈妈->学习、妹妹->吃。当小明有一下举动他们会得到通知。那么我们来运行一下看看效果。

try{
    $xiaoming = new Xiaoming;
    $grandpa = new Grandpa;
    $father = new Father;
    $mother = new Mother;
    $sister = new Sister;
    $xiaoming->attach($grandpa);
    $xiaoming->attach($father);
    $xiaoming->attach($mother);
    $xiaoming->attach($sister);
    $xiaoming->Sleep(0);
    $xiaoming->Eat(1);
    $xiaoming->Eat(0);
    $xiaoming->Play(1);
    $xiaoming->Play(0);
    $xiaoming->Study(1);
    $xiaoming->Study(0);
    $xiaoming->Sleep(1);
}
catch (Exception $e) {
    echo $e->getMessage();
}
这里运行的结果是:
  爷爷观察小明起床了
  妹妹观察小明在吃零食
  爸爸观察小明在玩游戏
  妈妈观察小明在学习
  爷爷观察小明在睡觉

这样我们观察者设计模式就快速的实现啦~~~~~不过上面还有一个我挖的坑,那就是我们如果想用“推”的方式获取状态该怎么实现?我们之前修改了被观察者,下面我们用观察者爸爸简单的说一下,其他观察者同上。先说一下原理,PHP 在方法调用上有这样的特性,只要给定的参数(实参)不少于定义时指定的必选参数(没有默认值的参数),PHP 就不会报错。传入一个方法的参数个数,可以通过 func_num_args() 函数获取;多余的参数可以使用 func_get_arg()函数读取。注意该函数是从 0 开始计数的,即 0 表示第 1 个实参。利用这个小技巧,update()方法可以通过 func_get_arg(1)接收小明的状态了。

class Father implements SplObserver
{
    public function update(SplSubject $subject)
    {
        if (func_num_args() === 2) { 
            $xiaomingInfo = func_get_arg(1); 
            if(xiaomingInfo['Play'] == 1){
                echo '爸爸观察小明在玩游戏<br/>';
            }
        } 
    }
}

以上就是我们本文说的观察者模式了,希望看到的小伙伴们可以将它运用起来,我觉得很不错,我们公司的邮件我就是利用它做的,扩展性我觉得很不错。希望大家可以学到东西。

本人博客地址:https://www.cwb763.com
本文借鉴:https://www.ibm.com/developerworks/cn/opensource/os-cn-observerspl