观察者模式详解

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(‘[email protected]‘, ‘张三‘, ‘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
向 [email protected] 发送电子邮件成功。内容是:你好张三你的新密码是 123456,请妥善保管
向 13610002000 发送短消息成功。内容是:你好张三你的新密码是 123456,请妥善保管

 User::resetPassword
向 [email protected] 发送电子邮件成功。内容是:你好张三你的新密码是 363989,请妥善保管
向 13610002000 发送短消息成功。内容是:你好张三你的新密码是 363989,请妥善保管
这是 1 封站内小纸条。你好张三,你的新密码是 363989,请妥善保管

 User::changePassword
向 [email protected] 发送电子邮件成功。内容是:你好张三你的新密码是 654321,请妥善保管
这是 1 封站内小纸条。你好张三,你的新密码是 654321,请妥善保管

我们看到,用户 张三 可以通过多种手段知道他的密码是什么。

时间: 2025-01-05 19:27:38

观察者模式详解的相关文章

jQuery的观察者模式详解 转载

jQuery的观察者模式详解 投稿:hebedich 本文主要是介绍了jQuery中on方法及trigger方法,以及围绕这个方法来体验的观察者模式,是篇非常不错的文章,对我们理解观察者模式很有帮助. undefined undefined ■ on方法绑定内置事件,自然触发 比如,我们给页面的body元素绑定一个click事件,这样写. <head> <meta http-equiv="Content-Type" content="text/html; c

Java设计模式----观察者模式详解

[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/3899208.html 联系方式:[email protected] [正文] 一.观察者模式的定义: 简单地说,观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象.这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新.例如:GUI中的事件

java观察者模式详解

简单地说,观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监察一个主题对象.这样一个主题对象在状态上的变化能够通知所有的依赖于此对象的那些观察者对象,使这些观察者对象能够自动更新. 观察者模式的结构 观察者(Observer)模式是对象的行为型模式,又叫做发表-订阅(Publish/Subscribe)模式.模型-视图(Model/View)模式.源-收听者(Source/Listener)模式或从属者(Dependents)模式. 本模式的类图结构如下: 图1.观察者模式的静态结构

设计模式之观察者模式详解

观察者模式又称为发布-订阅模式,涉及的角色有: ● 抽象主题 ● 具体主题 ● 抽象观察者 ● 具体观察者 案例演示:公司发放工资的时候通知所有观察者工资已发放. /** * 抽象观察者 */ public interface IWatcher { public void getNotice(String str); } /** * 抽象主题:工资接口 */ public interface ISalary { public void addWatcher(IWatcher watcher);

android设计模式之观察者模式详解

背景:有一个叫什么四人帮的:美国四个it大牛:我也不认识:总结出了23中设计模式:针对编程中的问题解决方案: 解决问题:当目标或数据发生变化时:观察者(界面)要显示通知: 观察者模式: 概念:一个对象发生改变时,所有信赖于它的对象自动做相应改变. 我在这里举一个西游记的例子: 被观察者:唐僧: 观察者:悟空:沙僧:八戒: 妖怪: 当师父唐僧被妖怪抓走时:观察者:悟空:沙僧:八戒:就会发现:并作出响应: 再举一个程序中的例子: 我们查询数据库并且用listview显示:而当数据库中的内容发生改变时

(六)观察者模式详解(包含观察者模式JDK的漏洞以及事件驱动模型)决了当时的问题,那时LZ接触JAVA刚几个月,比葫芦画瓢的用了观察者模式。

本章我们讨论一个除前面的单例以及代理模式之外,一个WEB项目中有可能用到的设计模式,即观察者模式. 说起观察者模式,LZ还是非常激动的,当初这算是第一个让LZ感受到设计模式强大的家伙.当初LZ要做一个小型WEB项目,要上传给服务器文件,一个需求就是要显示上传进度,LZ就是用这个模式解决了当时的问题,那时LZ接触JAVA刚几个月,比葫芦画瓢的用了观察者模式. 现在谈及观察者模式,能用到的地方就相对较多了,通常意义上如果一个对象状态的改变需要通知很多对这个对象关注的一系列对象,就可以使用观察者模式.

设计模式 - 观察者模式(Observer Pattern) 详解

观察者模式(Observer Pattern) 详解 本文地址: http://blog.csdn.net/caroline_wendy/article/details/26583157 版权所有, 禁止转载, 如有转载, 请站内联系. 观察者模式(Observer Pattern): 定义了对象之间的一对多的依赖, 这样一来, 当一个对象改变状态时, 它的所有依赖者都会收到通知并自动更新. 使用方法: 1. 首先新建主题(subject)接口, 负责注册(register)\删除(remove

Java--设计模式详解(23种)

一.设计模式的理解 刚开始“不懂”为什么要把很简单的东西搞得那么复杂.后来随着软件开发经验的增加才开始明白我所看到的“复杂”恰恰就是设计模式的精髓所在,我所理解的“简单”就是一把钥匙开一把锁的模式,目的仅仅是着眼于解决现在的问题,而设计模式的“复杂”就在于它是要构造一个“万能钥匙”,目的是提出一种对所有锁的开锁方案.在真正理解设计模式之前我一直在编写“简单”的代码.这个“简单”不是功能的简单,而是设计的简单.简单的设计意味着缺少灵活性,代码很钢硬,只在这个项目里有用,拿到其它的项目中就是垃圾,我

iOS开发——实用篇&amp;KVO与KVC详解

KVO与KVC详解 由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby.Python的动态特性,例如动态类型.动态加载.动态绑定等.今天我们着重介绍ObjC中的键值编码(KVC).键值监听(KVO)特性: 键值编码KVC 键值监听KVO 键值编码KVC 我们知道在C#中可以通过反射读写一个对象的属性,有时候这种方式特别方便,因为你可以利用字符串的方式去动态控制一个对象.其实由于ObjC的语言特性,你根部不必进行任何操作就可以进行属性的动态读写,这种方式就是Key Valu