【深入PHP 面向对象】读书笔记(十三) - 执行及描述任务(三) - 观察者模式

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 中了解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值