系统中的每个类应将重点放在某一个功能上,而不是其他方面。一个对象只做一件事情,并且将他做好。
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,有可能导致其它依赖对象的修改更新,那么开发任务会很快变成一个产生bug和消除bug的恶性循环。当我们创建一个对象的时候,一个对象的创建应当尽可能减少和其它对象间的耦合!一个对象的改变尽可能的不会引起代码库其它地方的修改。使用观察者模式能有效的解决此问题,一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知并被自动更新。
观察者模式(有时又被称为模型-视图(View)模式、源-收听者(Listener)模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常通过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。
假设一个负责处理用户登录的类:
class Login { const LOGIN_USER_UNKNOWN = 1; const LOGIN_WRONG_PASS = 2; const LOGIN_ACCESS = 3; private $_status = array(); public function handleLogin($user, $pass, $ip) { switch (rand(1,3)) { case self::LOGIN_ACCESS: $this->setStatus(self::LOGIN_ACCESS, $user, $ip); $ret = true; break; case self::LOGIN_WRONG_PASS: $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip); $ret = false; break; case self::LOGIN_USER_UNKNOWN: default: $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); } public function getStatus() { return $this->_status; } } $login = new Login(); $login->handleLogin('BNDong', '123456', '127.0.0.1'); var_dump($login->getStatus());
当然这个类并没有实际功能, handleLogin 方法会存储验证用户数据,该方法有3个潜在的结果。状态标签会被设置为 LOGIN_USER_UNKNOWN 、 LOGIN_WRONG_PASS 或 LOGIN_ACCESS 。
现在看上去还可以,但是一个登录组件可不可能只有这面点东西,我们试着增加需求:(代码的腐败就是不断的迭代出来的)
记录登录IP地址
public function handleLogin($user, $pass, $ip) { ... Logger::logIp($user, $ip, $this->getStatus()); ... }
登录失败发送邮件通知管理员
public function handleLogin($user, $pass, $ip) { ... !$ret && Notifier::mailWarning($user, $ip, $this->getStatus()); ... }
当然这些都是简单的功能,但是依这种方式来处理 Login 类,会发现该类和系统的依赖越来越深,代码的扩展和复用性越来越差! handleLogin 处理的东西越来越多。
实现
观察者模式的核心是把客户元素(观察者)从一个中心类(主体)中分离开来。当主体知道事件发生时,观察者需要被通知到。同时,我们并不希望将主体与观察者之间的关系进行硬编码。
为了达到这个目的,我们允许观察者在主体上进行注册。
interface Observable { public function attach(Observer $observer); public function detach(Observer $observer); public function notify(); } class Login implements Observable { const LOGIN_USER_UNKNOWN = 1; const LOGIN_WRONG_PASS = 2; const LOGIN_ACCESS = 3; private $_status = array(); private $_observers; public function __construct() { $this->_observers = array(); } public function handleLogin($user, $pass, $ip) { switch (rand(1,3)) { case self::LOGIN_ACCESS: $this->setStatus(self::LOGIN_ACCESS, $user, $ip); $ret = true; break; case self::LOGIN_WRONG_PASS: $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip); $ret = false; break; case self::LOGIN_USER_UNKNOWN: default: $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); } public function getStatus() { return $this->_status; } public function attach(Observer $observer) { $this->_observers[] = $observer; } public function detach(Observer $observer) { $newobservers = array(); foreach ($this->_observers as $obs) { if ($obs !== $observer) { $newobservers[] = $obs; } } $this->_observers = $newobservers; } public function notify() { foreach ($this->_observers as $obs) { $obs->update($this); } } }
现在 Login 类管理着一系列观察者对象。这些观察者可以由第三方通过 attach 方法添加进 Login 类,也可以通过 detach 方法来移除。 notify 方法用来告诉观察者一些相关事情发生了。 notify 方法会遍历观察者列表,调用每个观察者的 update 方法。
Login 类在它的 handleLogin 方法中调用 notify 方法。然后定义 Observer 接口,任何实现这个接口的对象都可以通过 attach 方法加入 Login 类中。
interface Observer { public function update(Observable $observable); } class SecurityMonitor implements Observer { public function update(Observable $observable) { $status = $observable->getStatus(); if ($status[0] == Login::LOGIN_WRONG_PASS) { // 发送邮件给系统管理员 print __CLASS__.":发送邮件给系统给管理员<br>"; } } } $login = new Login(); $login->attach(new SecurityMonitor()); $login->handleLogin('BNDong', '123456', '127.0.0.1');
至此实现了一个观察者模式,减少了各个对象之间的耦合。
优化
这里还存在一个问题,获取主体类状态是通过 getStatus 方法来获取的,而并不能判断调用的 getStatus 方法是存在并且可用的,所以要解决这个问题。
第一种方法:修改接口 Observer 中 update 方法参数 $observable 类型约束为 Login ,但是这样整个结构就被一个类限制了,多个登录类不能兼容,所以不推荐!!
第二种方法:在接口 Observable 中添加 getStatus 方法,但是这样会失去接口的通用性!!
第三种方法:继续保持 Observable 接口的通用性,将会添加 Observer 类型的对象来执行一些它们共有的任务。
下面针对第三种方法来优化上面的代码:
使用自建类优化
创建一个抽象超类:
abstract class LoginObserver implements Observer { private $_login; public function __construct(Login $login) { $this->_login = $login; $login->attach($this); } public function update(Observable $observable) { if ($observable == $this->_login) { $this->doUpdate($observable); } } abstract protected function doUpdate(Login $login); }
LoginObserver 类的构造函数需要一个 Login 对象作为参数。 LoginObserver 保存对 Login 对象的引用,并且调用 Login::attach() 方法。当 update 方法被调用时, LoginObserver 会检查参数传入的 Observable 对象是否是正确的引用,然后 LoginObserver 会调用模板方法 doUpdate 。现在可以创建一批 LoginObserver 对象,它们能够判断使用的是 Login 对象,而不是任意 Observable 对象:
class SecurityMonitor extends LoginObserver { public function doUpdate(Login $login) { $status = $login->getStatus(); if ($status[0] == Login::LOGIN_WRONG_PASS) { // 发送邮件给系统管理员 print __CLASS__.":发送邮件给系统给管理员<br>"; } } } class GeneralLogger extends LoginObserver { public function doUpdate(Login $login) { $status = $login->getStatus(); // 记录登录数据到日志 print __CLASS__.":记录登录数据到日志<br>"; } } $login = new Login(); new SecurityMonitor($login); new GeneralLogger($login); $login->handleLogin('BNDong', '123456', '127.0.0.1');
因此在主体类和观察者之间创建了一个很灵活的关系。
使用PHP内置SPL优化
PHP通过内置的SPL(Standard PHP Library,PHP标准类)扩展提供了对观察者模式的原生支持。其中的观察者(Observer)由3个元素组成:SplObserver、SplSubject 和 SplObjectStorage。SplObserver 和 SplSubject 都是接口,与之前示例中的 Observer 和 Observable 接口完全相同。SplObjectStorage 是一个工具类,用于更好的存储对象和删除对象。
/** * The <b>SplSubject</b> interface is used alongside * <b>SplObserver</b> to implement the Observer Design Pattern. * @link http://php.net/manual/en/class.splsubject.php */ interface SplSubject { /** * Attach an SplObserver * @link http://php.net/manual/en/splsubject.attach.php * @param SplObserver $observer <p> * The <b>SplObserver</b> to attach. * </p> * @return void * @since 5.1.0 */ public function attach (SplObserver $observer); /** * Detach an observer * @link http://php.net/manual/en/splsubject.detach.php * @param SplObserver $observer <p> * The <b>SplObserver</b> to detach. * </p> * @return void * @since 5.1.0 */ public function detach (SplObserver $observer); /** * Notify an observer * @link http://php.net/manual/en/splsubject.notify.php * @return void * @since 5.1.0 */ public function notify (); }