利用 SPL 快速实现 Observer 设计模式

目录:php

1.什么是 SPLweb

2.SplSubject 和 SplObserver 接口设计模式

3.为何使用 SplObjectStorage 类数组

4.模拟案例浏览器

5.结束语数据结构

6.下载资源app

什么是 SPL

SPL(Standard PHP Library)即标准 PHP 库,是 PHP 5 在面向对象上能力提高的真实写照,它由一系列内置的类、接口和函数构成。SPL 经过加入集合,迭代器,新的异常类型,文件和数据处理类等提高了 PHP 语言的生产力。它还提供了一些十分有用的特性,如本文要介绍的内置 Observer 设计模式。dom

本文介绍如何经过使用 SPL 提供的 SplSubject和 SplObserver接口以及 SplObjectStorage类,快速实现 Observer 设计模式。jsp

SPL 在大多数 PHP 5 系统上都是默认开启的,尽管如此,因为 SPL 的功能在 PHP 5.2 版本发生了引人注目的改进,因此建议读者在实践本文内容时,使用不低于 PHP 5.2 的版本。函数

SplSubject 和 SplObserver 接口

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

表 1. SplSubject 接口中的方法

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

表 2. SplObserver 中的方法

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

为何使用 SplObjectStorage 类

SplObjectStorage类实现了以对象为键的映射(map)或对象的集合(若是忽略做为键的对象所对应的数据)这种数据结构。这个类的实例很像一个数组,可是它所存放的对象都是惟一的。这个特色就为快速实现 Observer 设计模式贡献了很多力量,由于咱们不但愿同一个观察者被注册屡次。该类的另外一个特色是,能够直接从中删除指定的对象,而不须要遍历或搜索整个集合。

SplObjectStorage类的实例之因此可以只存储惟一的对象,是由于其 SplObjectStorage::attach()方法的实现中先判断了指定的对象是否已经被存储:

清单 1. SplObjectStorage::attach() 方法的部分源代码
function attach($obj, $inf = NULL) 
{ 
   if (is_object($obj) && !$this->contains($obj)) 
   { 
       $this->storage[] = array($obj, $inf); 
   } 
}

模拟案例

下面咱们经过一个模拟案例来演示 SPL 在实现 Observer 设计模式上的威力。该案例模拟了一个网站的用户管理模块,该模块包括 3 个主要功能:

  • 新增 1 个用户
  • 把指定用户的密码变动为他所指定的新密码
  • 在用户忘记密码时重置其密码

每当这些功能完成后,都须要将密码告知用户。除了传统的向用户发送 Email 这种手段外,咱们还须要向用户的手机发送短信,让他们更加方便地知道密码是什么。假设咱们的网站还有一套站内的消息系统,咱们称之为小纸条,在用户变动或重置密码后,向他们发送小纸条会令他们高兴的。

通过分析,该案例适合使用 Observer 设计模式解决,由于将密码告知用户的多种手段与用户密码的改变——不管是从无到有,用户主动变动,仍是系统重置——造成了多对一的关系。

咱们决定定义一个 User 类表示用户,实现需求中的 3 个功能。该类就是 Observer 设计模式中的目标(Subject)角色。咱们还须要一组类,实现利用各类手段向用户发送新密码的功能,这些类就充当了 Observer 设计模式中的观察者(Observer)角色。

通过简单地分析后,咱们画出 UML 类图:

图 1. 模拟案例的 UML 类图

 

根据 UML 类图,首先,定义 1 个名为 User 的类模拟案例中的用户。尽管实际网站中的用户要有更多的属性,特别是一般须要用 ID 来标识每一个用户,可是咱们为了突出本文的主题,只保留了案例所需的属性。

清单 2. User 类的源代码
<?php
 
class User implements SplSubject { 
 
   private $email; 
   private $username; 
   private $mobile; 
   private $password; 
   /** 
    * @var SplObjectStorage 
    */ 
   private $observers = NULL; 
 
   public function __construct($email, $username, $mobile, $password) { 
       $this->email = $email; 
       $this->username = $username; 
       $this->mobile = $mobile; 
       $this->password = $password; 
 
       $this->observers = new SplObjectStorage(); 
   } 
 
   public function attach(SplObserver $observer) { 
       $this->observers->attach($observer); 
   } 
 
   public function detach(SplObserver $observer) { 
       $this->observers->detach($observer);
   } 
 
   public function notify() { 
       $userInfo = array( 
           'username' => $this->username, 
           'password' => $this->password, 
           'email' => $this->email, 
           'mobile' => $this->mobile, 
       ); 
       foreach ($this->observers as $observer) { 
           $observer->update($this, $userInfo); 
       } 
   } 
 
   public function create() { 
       echo __METHOD__, PHP_EOL; 
       $this->notify(); 
   } 
 
   public function changePassword($newPassword) { 
       echo __METHOD__, PHP_EOL; 
       $this->password = $newPassword; 
       $this->notify(); 
   } 
 
   public function resetPassword() { 
       echo __METHOD__, PHP_EOL; 
       $this->password = mt_rand(100000, 999999); 
       $this->notify(); 
   } 
 
}

User 类要想充当目标角色,就须要实现 SplSubject接口,而按照实现接口的法则,attach()detach()和 notify()就必须被实现。请注意,因为在 SplSubject接口中,attach() 和detach() 的参数都使用了类型提示(type hinting),在实现这两个方法时,也不能省略参数前面的类型。咱们还使用了 $observers实例属性保存一个 SplObjectStorage对象,用来存放全部注册上来的观察者。

的确,一个数组就能解决问题,可是很快就能够发现,使用了 SplObjectStorage以后删除一个观察者实现起来是多么简单,直接委托给 SplObjectStorage对象!是的,不须要再使用最原始的 for语句遍历观察者数组或者使用 array_search函数,1 行搞定。

接下来分别定义充当观察者角色的 3 个信息发送类。为了简单,咱们只是经过输出文原本伪装发送信息。可即便是伪装,依然须要知道用户的信息。可看看 SplObserver接口 update()方法的签名,多么使人沮丧,它没法接受目标角色经过调用其 notify() 方法发送通告时给出的参数。若是你试图在重写 update()方法时加上第 2 个参数,会获得一个相似

Fatal error: Declaration of EmailSender::update() must be compatible with that of SplObserver::update() 的错误而使代码执行终止。

其实,当目标所持有的状态(在本例中是用户的密码)更新时,如何通知观察者有两种方法。“拉”的方法和“推”的方法。SPL 使用的是“拉”的方法,观察者须要经过目标的引用(做为 update()方法的参数传入)来访问其属性。“拉”的方法须要让观察者更了解目标都拥有哪些属性,这增长了它们耦合度。并且主题也要对观察者门户大开,违背了封装性。解决的方法是在目标中提供一系列 getter 方法,如 getPassword()来让观察者得到用户的密码。

虽然“拉”的方法可能被认为更加正确,可是咱们以为让主题把用户的信息“推”过来更加方便。既然经过在重写 update()方法时加上第 2 个参数是行不通的,那么就从别的方向上着手。好在 PHP 在方法调用上有这样的特性,只要给定的参数(实参)很多于定义时指定的必选参数(没有默认值的参数),PHP 就不会报错。传入一个方法的参数个数,能够经过 func_num_args() 函数获取;多余的参数可使用 func_get_arg()函数读取。注意该函数是从 0 开始计数的,即 0 表示第 1 个实参。利用这个小技巧,update()方法能够经过 func_get_arg(1)接收一个用户信息的数组,有了这个数组,就能知道邮件该发给谁,新密码是什么了。为了节约篇幅,并且三个信息发送类很是相像,下面只给出其中一个的源代码,完整的源代码能够下载本文的附件获得。

清单 3. Email_Sender 类的源代码
<?php 
 
class EmailSender implements SplObserver { 
 
   public function update(SplSubject $subject) { 
       if (func_num_args() === 2) { 
           $userInfo = func_get_arg(1); 
           echo "向 {$userInfo['email']} 发送电子邮件成功。内容是:你好 {$userInfo['username']}" . 
           "你的新密码是 {$userInfo['password']},请妥善保管", PHP_EOL; 
       } 
   } 
 
}

最后咱们写一个测试脚本 test.php。建议使用 CLI 的方式 php – f test.php来执行该脚本,但因为设置了 Content-Type响应头部字段为 text/plain,在浏览器中应该也能看到一行一行显示的结果(由于没有用 <br />作换行符而是使用常量 PHP_EOL,因此不设置 Content-Type的话,就不能正确分行显示了)。

清单 4. 用于测试的脚本
<?php
 
header('Content-Type: text/plain'); 
 
function __autoload($class_name) { 
   require_once "$class_name.php"; 
} 
 
$email_sender = new EmailSender(); 
$mobile_sender = new MobileSender(); 
$web_sender = new WebsiteSender(); 
 
$user = new User('user1@domain.com', '张三', '13610002000', '123456'); 
 
// 建立用户时经过 Email 和手机短信通知用户
$user->attach($email_sender); 
$user->attach($mobile_sender); 
$user->create($user); 
echo PHP_EOL; 
 
// 用户忘记密码后重置密码,还须要经过站内小纸条通知用户
$user->attach($web_sender); 
$user->resetPassword(); 
echo PHP_EOL; 
 
// 用户变动了密码,可是不要给他的手机发短信
$user->detach($mobile_sender); 
$user->changePassword('654321'); 
echo PHP_EOL;
清单 5. 运行结果
User::create 
向 user1@domain.com 发送电子邮件成功。内容是:你好张三你的新密码是 123456,请妥善保管
向 13610002000 发送短消息成功。内容是:你好张三你的新密码是 123456,请妥善保管
 
 User::resetPassword 
向 user1@domain.com 发送电子邮件成功。内容是:你好张三你的新密码是 363989,请妥善保管
向 13610002000 发送短消息成功。内容是:你好张三你的新密码是 363989,请妥善保管
这是 1 封站内小纸条。你好张三,你的新密码是 363989,请妥善保管
 
 User::changePassword 
向 user1@domain.com 发送电子邮件成功。内容是:你好张三你的新密码是 654321,请妥善保管
这是 1 封站内小纸条。你好张三,你的新密码是 654321,请妥善保管

咱们看到,用户 张三 能够经过多种手段知道他的密码是什么。

结束语

对于经验丰富的开发者,即便不使用 SPL 也能够轻松实现 Observer 设计模式,可是使用 SPL 带来了更高的效率,特别在结合了 SplObjectStorage以后,注册和删除观察者都由它的实例代理完成。虽然在使用“推”的方式更新 Observer 时,SplObserver的 update()方法只接受 1 个参数显得美中不足,或者说 SPL 内置的 Observer 设计模式只支持经过“拉模式”获取通知,可是经过本文的介绍的小技巧便可弥补。所以,SPL 在快速实现 Observer 设计模式上成为了首选。

下载资源

样例代码(observer_pattern.rar | 26KB)

原文地址:https://www.ibm.com/developerworks/cn/opensource/os-cn-observerspl/#ibm-pcon

相关文章
相关标签/搜索