深入PHP面向对象、模式与实践——执行及描述任务(3)

本文介绍了一种用于解耦系统组件的设计模式——观察者模式,并通过一个登录系统的例子展示了如何使用该模式来处理登录事件,包括错误通知和日志记录。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

观察者模式

创建依赖关系,当系统事件发生时通知观察者对象。

正交性是我们之前讨论过的一个话题。程序员的目标应该是创建在改动和转移时对其他组件影响最小的组件。当然,正交性通常只是个梦想,然而你可以使用不同的策略来使引用尽量减少。

下面假设有个负责处理用户登录的类:

class Login
{
    const LOGIN_USER_UNKNOW = 1;
    const LONGIN_WRONG_PASS = 2;
    const LOGIN_ACCESS = 3;
    private $status = array();

    function handleLogin($user, $pass, $ip)
    {
        switch (rand(1, 3)) {
            case 1:
                $this->setStatus(self::LOGIN_ACCESS, $user, $ip);
                $ret = true;
                break;
            case 2:
                $this->setStatus(self::LONGIN_WRONG_PASS, $user, $ip);
                $ret = false;
                break;
            case 3:
                $this->setStatus(self::LOGIN_USER_UNKNOW, $user, $ip);
                $ret = false;
                break;
        }
        Logger::logIP();
        return $ret;
    }

    private function setStatus($status, $user, $ip)
    {
        $this->status = array($status, $user, $ip);
    }

    function getStatus()
    {
        return $this->status;
    }
}

考虑到到安全问题,系统管理员可能会要求有用户登录失败时发一封邮件到管理员的邮箱。你可以再一次回到登录方法并添加一个新的调用:

if (!$ret) {
    Notifier::mailWarning($user, $ip, $this->getStatus());
}

随着新功能的不断加入,这个类会紧紧嵌入到系统中。那么我们该如何拯救呢?观察者模式用在这里最合适不过了。

实现:

观察者模式的核心是把客户元素(观察者)从一个中心类(主体)中分离开来。当主体知道事件发生时,观察者需要被通知到。同时,我们并不希望主体与观察者之间的关系进行硬编码。

interface Observable
{
    function attach(Observer $observer);

    function detach(Observer $observer);

    function notify();
}

class Login implements Observable
{

    private $observers;
    const LOGIN_USER_UNKNOW = 1;
    const LONGIN_WRONG_PASS = 2;
    const LOGIN_ACCESS = 3;
    private $status = array();

    function __construct()
    {
        $this->observers = array();
    }

    function handleLogin($user, $pass, $ip)
    {
        switch (rand(1, 3)) {
            case 1:
                $this->setStatus(self::LOGIN_ACCESS, $user, $ip);
                $ret = true;
                break;
            case 2:
                $this->setStatus(self::LONGIN_WRONG_PASS, $user, $ip);
                $ret = false;
                break;
            case 3:
                $this->setStatus(self::LOGIN_USER_UNKNOW, $user, $ip);
                $ret = false;
                break;
        }
        $this->notify();
        return $ret;
    }

    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);
        }
    }

    private function setStatus($status, $user, $ip)
    {
        $this->status = array($status, $user, $ip);
    }

    function getStatus()
    {
        return $this->status;
    }
}

然后让我们定义Observer接口:

interface Observer
{
    function update(Observable $observable);
}

class SecurityMonitor extends Observer
{
    function update(Observable $observable)
    {
        $status = $observable->getStatus();
        if ($status[0] == Login::LONGIN_WRONG_PASS) {
            //发送邮件
            print __CLASS__ . ":\tsending mail to sysadmin\n";
        }
    }
}

$login = new Login();
$login->attach(new SecurityMonitor());

虽然调用发生在一个Observable对象上,但无法保证该对象也是一个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($observable);
        }
    }

    abstract function doUpdate(Login $login);
}

class SecurityMonitor extends LoginObserver
{
    function doUpdate(Login $login)
    {
        $status = $login->getStatus();
        if ($status[0] == Login::LONGIN_WRONG_PASS) {
            //发送邮件
            print __CLASS__ . ":\tsending mail to sysadmin\n";
        }
    }
}

class GeneralLogger extends LoginObserver
{
    function doUpdate(Login $login)
    {
        $status = $login->getStatus();
        if ($status[0] == Login::LONGIN_WRONG_PASS) {
            //记录登录数据到日志
            print __CLASS__ . ":\tadd login data to log\n";
        }
    }
}

class PartnershipTool extends LoginObserver
{
    function doUpdate(Login $login)
    {
        $status = $login->getStatus();
        if ($status[0] == Login::LONGIN_WRONG_PASS) {
            //检查IP地址,如果匹配列表,则设置cookie
            print __CLASS__ . ":\tset cookie if IP matches a list\n";
        }
    }
}

当实例化对象时,创建和添加LoginObserver类的任务就完成了:

$login = new Login();
new SecurityMonitor($login);
new GeneralLogger($login);
new PartnershipTool($login);

这里写图片描述
PHP通过内置的SPL扩展提供了对观察者模式的原生支持。SPL是一套可以帮助程序员处理很多面向对象问题的工具。下面是一个改进过的示例代码:

class Login implements SplSubject
{
    private $storage;

    //...

    function __construct()
    {
        $this->storage = new SplObjectStorage();
    }

    function attach(SplObserver $observer)
    {
        $this->storage->attach($observer);
    }

    public function detach(SplObserver $observer)
    {
        $this->storage->detach($observer);
    }

    function notify()
    {
        foreach ($this->storage as $obs) {
            $obs->update($this);
        }
    }
    //...
}

abstract class LoginObserver implements SplObserver
{
    private $login;

    function __construct(Login $login)
    {
        $this->login = $login;
        $login->attach($this);
    }

    function update(SplSubject $subject)
    {
        if ($subject === $this->login) {
            $this->doUpdate($subject);
        }
    }

    abstract function doUpdate(Login $login);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值