php 面向对象开发中的观察者模式介绍

           观察者模式可以通过灵巧的方式减少各个组件之间的联系。使各个类专注于做自己的事情。在类的扩展功能需要调整修改时而不用频繁的修改主流程代码。

比如一个用户注册后需要产生的相关动作:如发送短信,发送邮件,写文本日志等的需求。

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

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

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

经过分析,该案例适合使用 Observer 设计模式解决,因为将密码告知用户的多种手段与用户密码的改变——无论是从无到有,用户主动变更,还是系统重置——形成了多对一的关系。

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

经过简单地分析后,我们画出 UML 类图:


图 1. 模拟案例的 UML 类图
图 1. 模拟案例的 UML 类图

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


清单 2. User 类的源代码
 <?php 
    class User implements SplSubject { 
	private $email; 
	private $username; 
	private $mobile; 
	private $password; 
	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,请妥善保管
 

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


来自91CTO

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值