11.3 观察者模式
11.3.2 问题
程序员的目标应该是创建在改动和转移时其他组件影响最小的组件。如果对一个组件所做的改变会引起代码库其他地方的一连串改变,那么开发任务会很快变成一个产生 bug 和消除 bug 的恶性循环。
当然,要完全消除组件间的关联是理论的,在实践开发过程中,系统中的组件比如包含着对其他组件的引用,我们需要通过一些策略来使引用尽量减少。
假设下面是一个负责负责处理用户登录的类:
class Login {
const LOGIN_USER_UNKNOWN = 1;
const LOGIN_WRONG_PASS = 2;
const LOGIN_ACCESS = 3;
private $status = array();
/* 使用存储机制来验证用户数据 */
function handleLogin($user, $pass, $ip) {
/* 使用rand()函数来模拟登陆可能发生的三种情况 */
switch (rand(1,3)) {
case 1:
$this->setStatus(self::LOGIN_ACCESS, $user, $ip);
$ret = true;
break;
case 2:
$this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
$ret = false;
break;
case 3:
$this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
$ret = false;
break;
}
return $ret;
}
private function setStatus($status, $user, $ip) {
$this->status = array($status, $user, $ip);
}
private function getStatus() {
return $this->status;
}
}
在这个实际的例子中,handleLogin() 方法会使用存储机制来验证用户数据,通过rand() 函数来模拟登陆的过程,会有三种潜在的可能,分别通过将状态标签设置为 LOGING_ACCESS、LOGIN_WRONG_PASS 和 LOGIN_USER_UNKNOWN。
接下来我们因为具体业务需要,需要添加一些功能。
首先,我们需要在用户登陆的时候将用户登陆的 IP 地址记录到日志中(通过调用外部的一个 Logger 类):
/* 使用存储机制来验证用户数据 */
function handleLogin($user, $pass, $ip) {
/* 使用rand()函数来模拟登陆可能发生的三种情况 */
switch (rand(1,3)) {
case 1:
$this->setStatus(self::LOGIN_ACCESS, $user, $ip);
$ret = true;
break;
case 2:
$this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
$ret = false;
break;
case 3:
$this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
$ret = false;
break;
}
/* 记录日志 */
Logger::logIP($user, $ip, $this->getStatus());
return $ret;
}
考虑到安全的问题,需要在用户登陆失败时发一封邮件到管理员邮箱,可以考虑再一次回到登陆方法并添加一个新的调用:
/* 用户登录失败,向管理员发送一封邮件 */
if (!$ret) {
Notifier::mailWarning($user, $ip, $this->getStatus());
}
此时,这个方法完整的代码:
/* 使用存储机制来验证用户数据 */
function handleLogin($user, $pass, $ip) {
/* 使用rand()函数来模拟登陆可能发生的三种情况 */
switch (rand(1,3)) {
case 1:
$this->setStatus(self::LOGIN_ACCESS, $user, $ip);
$ret = true;
break;
case 2:
$this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
$ret = false;
break;
case 3:
$this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
$ret = false;
break;
}
/* 记录日志 */
Logger::logIP($user, $ip, $this->getStatus());
/* 用户登录失败,向管理员发送一封邮件 */
if (!$ret) {
Notifier::mailWarning($user, $ip, $this->getStatus());
}
return $ret;
}
除此之外,还会有更多的需求会被后续添加进来,如果直接在代码中加入功能来满足需求,会破坏我们的设计。Login 类很快会紧紧嵌入到这个特殊的系统中,如果没有逐行仔细检查代码,然后移除特别针对旧系统的一切代码,就无法把它提取到其他产品上,然后我们就走上了剪切粘贴代码的开发道路了。
11.3.2 实现
使用观察者模式。观察者模式的核心是把客户元素(观察者)从一个中心类(主体)中分离出来。当主体知道事件发生时,观察者需要被通知到。同时,观察者和主体之间不能进行硬编码。
为了达到这个目的,我们让观察者在主体上进行注册。Login类强制使用Observab接口,实现3个新方法attach() detach() notify()。
/* Observable接口 */
interface Observable {
function attach(Observer $observer);
function detach(Observer $observer);
function notify();
}
/* Login类为主体,用于管理观察者,将观察者保存在$observers中
并添加三个新方法attach() detach() notify()
attach()方法将观察者添加进Login类
detach()方法将添加进的观察者移除
notify()方法用来向观察者发送通知
*/
class Login implements Observable {
private $observers;
function __construct() {
$this->observers = array();
}
function attach(Observer $observer) {
$this->observers[] = $observer;
}
function detach(Observer $observer) {
$newobservers = array();
foreach ($this->observers as $obs) {
if ($obs!==$observer) {
$newobservers[]=$obs;
}
}
$this->observers = $newobservers;
}
function notify() {
foreach ($this->observers as $obs) {
$obs->update($this);
}
}
}
现在,修改 handleLogin() 方法,使其调用 notify() 方法:
/* 使用存储机制来验证用户数据 */
function handleLogin($user, $pass, $ip) {
/* 使用rand()函数来模拟登陆可能发生的三种情况 */
switch (rand(1,3)) {
case 1:
$this->setStatus(self::LOGIN_ACCESS, $user, $ip);
$ret = true;
break;
case 2:
$this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
$ret = false;
break;
case 3:
$this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
$ret = false;
break;
}
/* 调用通知方法 */
$this->notify();
return $ret;
}
所以完整的 Login 类变成:
/* Login类为主体,用于管理观察者,将观察者保存在$observers中
并添加三个新方法attach() detach() notify()
attach()方法将观察者添加进Login类
detach()方法将添加进的观察者移除
notify()方法用来向观察者发送通知
*/
class Login implements Observable {
private $observers;
const LOGIN_USER_UNKNOWN = 1;
const LOGIN_WRONG_PASS = 2;
const LOGIN_ACCESS = 3;
private $status = array();
function __construct() {
$this->observers = array();
}
/* 使用存储机制来验证用户数据 */
function handleLogin($user, $pass, $ip) {
/* 使用rand()函数来模拟登陆可能发生的三种情况 */
switch (rand(1,3)) {
case 1:
$this->setStatus(self::LOGIN_ACCESS, $user, $ip);
$ret = true;
break;
case 2:
$this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
$ret = false;
break;
case 3:
$this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
$ret = false;
break;
}
/* 调用通知方法 */
$this->notify();
return $ret;
}
private function setStatus($status, $user, $ip) {
$this->status = array($status, $user, $ip);
}
private function getStatus() {
return $this->status;
}
function attach(Observer $observer) {
$this->observers[] = $observer;
}
function detach(Observer $observer) {
$newobservers = array();
foreach ($this->observers as $obs) {
if ($obs!==$observer) {
$newobservers[]=$obs;
}
}
$this->observers = $newobservers;
}
function notify() {
foreach ($this->observers as $obs) {
$obs->update($this);
}
}
}
接下来我们定义 Observer 接口:
interface Observer {
function update(Observable $Observable);
}
并且定义一个抽象类 LoginObsever :
/* LoginObserver 登录观察者类 该类要求传入一个Login类型的对象 */
abstract class LoginObserver implements Observer {
private $login;
function __construct(Login $login) {
$this->login = $login;
$login->attach($this);
}
function update(Observable $observable) {
if ($observable === $this->login) {
$this->doUpdate($this);
}
}
abstract function doUpdate(Login $login);
}
这个类的构造函数需要一个 Login 对象作为参数,并在类内部保存对 Login 对象的引用,并调用 Login::attach()方法,将这个 Obsever 对象也存储到 Login对象中。
这样,当 update() 被调用的时,LoginObserver 就会检查参数传入的 Observable 对象是否是正确的引用,然后 LoginObserver 会调用模板方法 doUpdate()。
接下来我们创建几个具体的 LoginObserver 类型的对象,因为继承自 LoginObserver 类,所以具有判断使用的是 Login 对象,而不是任意的 Observable 对象:
class SecurityMonitor extends LoginObserver {
function doUpdate(Login $login) {
$status = $login->getStatus();
if ($status[0]==Login::LOGIN_WRONG_PASS) {
// 发送邮件给系统管理员
echo __CLASS__.":sending mail to sysadmin.";
}
}
}
class GeneralLogger extends LoginObserver {
function doUpdate(Login $login) {
$status = $login->getStatus();
// 记录登录数据到日志
echo __CLASS__.":adding login data to log.";
}
}
class Partnershiptool extends LoginObserver {
function doUpdate(Login $login) {
$status = $login->getStatus();
// 检查IP地址,如果匹配列表,则设置cookie
echo __CLASS__.":set cookie if IP matches a list.";
}
}
此时,我们再实例化对象时,就能很好的完成任务了。
$login = new Login();
new SecurityMonitor($login);
new GeneralLogger($login);
new Partnershiptool($login);
此时,我们就将「安全检查」、「写日志」和「处理特殊用户」的功能注册到 Login 对象上了。或者说,我们想要将哪几个功能注册到对象,就能将哪几个功能注册到对象上,同时,扩展新的功能类也不会影响已经完成的功能类。当我们有用户登录的时候,就能完成这些功能的检查了。
整体上,我们在主体类和观察者之间创建了一个很灵活的关系:
在 PHP 中,内置了 SPL(Standard PHP Library,PHP标准类)扩展提供了对观察者模式的原生支持。其中的观察者(Observer)由3个元素组成:SplObserver、SplSubject 和 SplObjectStorage。SplObserver 和 SplSubject 都是接口,与我们自己创建的 Observer 和 Observable 接口完全相同。 SplObjectStorage 是一个工具类,用于更好地存储对象和删除对象。关于更多 SPL 的信息可以在 http://www.php.net/spl 中了解。