目录:

1.什么是 SPL

2.SplSubject 和 SplObserver 接口

3.为什么使用 SplObjectStorage 类

4.模拟案例

5.结束语

6.下载资源

什么是 SPL

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

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

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

SplSubject 和 SplObserver 接口

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

表 1. SplSubject 接口中的方法
方法声明 描述
abstract public void attach ( SplObserver $observer ) 添加(注册)一个观察者
abstract public void detach ( SplObserver $observer ) 删除一个观察者
abstract public void notify ( void ) 当状态发生改变时,通知所有观察者

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

表 2. SplObserver 中的方法
方法声明 描述
abstract public void update ( SplSubject $subject ) 在目标发生改变时接收目标发送的通知;当关注的目标调用其 notify()时被调用

该设计模式的核心思想是,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

利用 SPL 快速实现 Observer 设计模式的更多相关文章

  1. 利用Nodejs快速构建应用原型

    利用Nodejs快速构建应用原型 开发一个应用往往需要快速的构建原型,然后在此基础上设计和改进,前端可能立马能看到效果,但是后端业务逻辑不会那么快,这个时候其实我们需要额只是一些模拟数据,所以不需要真 ...

  2. Observer设计模式中-委托事件-应用在消息在窗体上显示

    Observer设计模式:监视者模式.在类中的方法中处理的结果或者消息通过事件委托 的方式发送给主窗体. 因为在其它类中直接访问主窗体类,显示内容是不能直接调用控件赋值的,当然也有别的类似查阅控件名, ...

  3. 转载:C#中委托、事件与Observer设计模式

    原文地址 http://www.tracefact.net/CSharp-Programming/Delegates-and-Events-in-CSharp.aspx 感谢博主分享! 范例说明 假设 ...

  4. 利用 TFLearn 快速搭建经典深度学习模型

      利用 TFLearn 快速搭建经典深度学习模型 使用 TensorFlow 一个最大的好处是可以用各种运算符(Ops)灵活构建计算图,同时可以支持自定义运算符(见本公众号早期文章<Tenso ...

  5. 利用flask-sqlacodegen快速导入ORM表结构

    利用flask-sqlacodegen快速导入ORM表结构 友情提示:如果是使用pymysql请预先pip install 哦~ 这是window下使用virtualenv环境下执行的 Linux用户 ...

  6. 四、利用EnterpriseFrameWork快速开发基于WCF为中间件的三层结构系统

    回<[开源]EnterpriseFrameWork框架系列文章索引> EnterpriseFrameWork框架实例源代码下载: 实例下载 本章内容与上一张<利用Enterprise ...

  7. 委托、事件与Observer设计模式

    范例说明 上面的例子已不足以再进行下面的讲解了,我们来看一个新的范例,因为之前已经介绍了很多的内容,所以本节的进度会稍微快一些: 假设我们有个高档的热水器,我们给它通上电,当水温超过95度的时候:1. ...

  8. 如何利用Excel快速批量生成想要的代码

    如何利用Excel快速批量生成想要的代码 使用场景 在HTML DOM Video 对象这个页面 我想要将所有的中文描述和对应的属性(共32个属性)打印出来--console.log(descript ...

  9. 利用Python快速绘制海报级别地图

    1 简介 基于Python中诸如matplotlib等功能丰富.自由度极高的绘图库,我们可以完成各种极富艺术感的可视化作品,关于这一点我在系列文章在模仿中精进数据可视化中已经带大家学习过很多案例了. ...

随机推荐

  1. HTML5实现本地JSON文件的读写

    参考: 使用HTML5来实现本地文件读取和写入  (FileReader读取json文件,FileSaver.js保存json文件) w3school <input>标签 FileRead ...

  2. OpenJDK与JDK的区别分析

    一.以下是具体分析: 以下内容转自http://fgh2011.iteye.com/blog/1771649: 使用过LINUX的人都应该知道,在大多数LINUX发行版本里,内置或者通过软件源安装JD ...

  3. wchar_t和char转化

    char* WcharToChar(const wchar_t* wp) { char *m_char; int len = WideCharToMultiByte(CP_ACP, 0, wp, wc ...

  4. MyEclipse启动Tomcat缓慢的原因及解决办法

    不知道朋友们是否有一种烦恼:有时候使用MyEclipse启动Tomcat十分缓慢,可能在几分钟前20秒以内,但现在却需要200秒开外:其间内存和CPU都被占用地厉害,而控制台的输出似乎有重复的迹象:而 ...

  5. 10.24CRM完成

    2018-10-24 16:16:59 已经完成了crm项目: 项目做完了,这几天可以安静整理Django啦!!!把博客整理一下! 然后再过几天针就可以回学校啦!! 今天程序员节  节日快乐!hell ...

  6. Thrift版本管理

    对于健壮的服务,其中的数据类型必须一种机制来对其进行版本管理,尤其是它可以在不中断服务(或者更坏的情况,出现段错误)的前提下,增加或删除一个对象中的字段,或者改变一个函数的参数列表. 字段标识符 Th ...

  7. Linux多进程和多线程的一次gdb调试实例

    转载https://typecodes.com/cseries/multilprocessthreadgdb.html 1 原文整理 默认设置下,在调试多进程程序时gdb只会调试主进程.gdb7以上的 ...

  8. eclipse安装Hadoop-0.20.2插件

    因为在使用Hadoop-0.20.2这个古董,需要使用它自带的eclipse插件,而我最初安装的是现代的eclipse4.10.0. 在经历两天,以及以下种种尝试之后,均以失败告终: 1.网上找适合的 ...

  9. iOS FMDB的是使用和注意事项

    1.FMDB 默认的使用方法不是线程安全的. 2.Sqlite 默认不支持外键. 3.Sqlite 不支持用 ALTER 关键字给已有表添加外键约束 解决: 1.FMDBDatabaseQueue 2 ...

  10. wfp(Application的运用)

    1.Application中封装了wpf的一些常用的功能,如检查未经处理的异常,处理命令行参数等. 2.如何启动wpf程序: app.xaml可用于启动wpf的窗体.也可以不用这用方式.直接创建一个a ...