设计模式第八讲-观察者模式

前言

设计模式停更了很久, 发现兜兜转转回来, 仍是离开不了那些个套路.php

今天咱们主要讲解下 观察者模式, 可能你听这个名字感受很熟, 若是给你说下还能够称它为 发布订阅 模式的话, 相信你对它就绝不陌生了.json

观察者模式定义了一种一对多的依赖关系, 让多个观察者对象同时监听某一个主题对象, 这个主题发生变化时, 通知全部的观察对象. 总的来讲, 观察者是解除耦合的重要手段.设计模式

V1 这个需求很简单, 怎么实现他无论

叮叮叮..安全

  • 产品经理: 我须要实现一个用户登录的接口, 这个接口很重要也很简单, 赶忙实现下, 上去一梭子搞完节前就上线.
  • 小明: 好的, 经理

登录代码:bash

class Login
{
    /**
     * 处理登录
     *
     * @return array
     */
    public function handleLogin($param)
    {

        $isLogin = false;
        //执行登录
        switch ($this->doLogin($param)) {
            case 0:
                $message = '登录成功';
                $isLogin = true;
                break;
            case 1:
                $message = '账号或密码不对';
                break;
            case 2:
                $message = '帐号已失效';
                break;
            default:
                $message = '登录失败';

        }

        return [
            'isLogin' => $isLogin,
            'message' => $message,
        ];
    }

    /**
     * 执行具体登录操做
     *
     * @return int
     */
    public function doLogin($param)
    {
        //dododo
        return rand(0, 2);
    }
}

复制代码

这里咱们为了演示, 在实际执行登录方法中随机返回 0~2,对应返回不回的提示信息.微信

执行以下数据结构

$result = (new Login)->handleLogin(['email'=>'1350495180@qq.com','passwd'=>'123456']);
echo json_encode($result, JSON_UNESCAPED_UNICODE);

output:
{"isLogin":false,"message":"账号或密码不对"}
{"isLogin":true,"message":"登录成功"}
{"isLogin":false,"message":"帐号已失效"}
复制代码

V2 咱们还得记录下登录的一些信息

叮叮叮...架构

  • 产品经理: 登录是实现了, 但咱们还须要一些数据用做分析, 这个需求一样很简单, 直接在上一次的登录代码那插入一个保存就能够测试

  • 小明: 好的呢经理.优化

public function handleLogin($param)
{
    $isLogin = false;
    //执行登录
    switch ($this->doLogin($param)) {
       .....
    }

    $param['isLogin'] = $message;
    $this->_saveLoginLog($param);
    
    return [
        'isLogin' => $isLogin,
        'message' => $message,
    ];
}

/**
 * 添加登录日志
 *
 * @param $param
 * @return bool
 */
private function _saveLoginLog($param)
{
    $param['client_ip'] = $this->get_real_ip();
    $this->loginLogModel->insert($param);
    return true;
}

复制代码

V3 咱们得给再作更多的事情

叮叮叮...

  • 产品经理: 仍是得升级下这个登录接口, 这个系统数据很重要, 咱们应该给系统管理员发邮件信息, 再给归属账号发条登录短信提醒安全系数就会更高了, 再在那加点逻辑, 这个功能不复杂.
  • 小明: 这个事情很差作, 这样改下去会很乱.
  • 产品经理: 大家怎么架构是大家的事情, 你实现这个功能须要多久
  • 小明: 经理给我一天能够吗
  • 产品经理: 我不认为这个事情有多难, 找个实习生最多一小时搞定, 这个功能很紧急, 若是作不了的话, 咱们能够把你领导叫来一块儿沟通下, 你的时间我接受不了

  • 小明: 别呀, 我试试还不行嘛.

开发至今, 我才发现这是一个愈来愈大的陷阱, 我没有意识到这点, 即便一个简单的登录接口, 每次改完都得重测一遍, 一直变来变去, 思考着该怎么去重构我得代码.

观察者模式 v1

class Login implements LoginSubjectInterface
{

    private $observers;

    public function __construct()
    {
        $this->observers = [];
    }

    /**
     * 加入观察者
     *
     * @param LoginObserverInterface $loginSubject
     */
    public function attach(LoginObserverInterface $loginObserver)
    {
        $this->observers[] = $loginObserver;
    }

    /**
     * 移除观察者
     *
     * @param LoginObserverInterface $loginObserver
     */
    public function detach(LoginObserverInterface $loginObserver)
    {
        //todo
    }

    /**
     * 通知观察者事件
     */
    public function notify()
    {
        foreach ($this->observers as $observer) {
            $observer->doNotify();
        }
    }
    public function notify()
    {
        foreach ($this->observers as $observer) {
            $observer->doNotify();
        }
    }

    /**
     * 处理登录
     *
     * @return array
     */
    public function handleLogin($param)
    {

        $isLogin = false;
        //执行登录
        switch ($this->doLogin($param)) {
            case 0:
                $message = '登录成功';
                $isLogin = true;
                break;
            case 1:
                $message = '账号或密码不对';
                break;
            case 2:
                $message = '帐号已经被禁用';
                break;
            default:
                $message = '登录失败';

        }

        $this->notify();

        return [
            'isLogin' => $isLogin,
            'message' => $message,
        ];
    }
}
复制代码

登录成功观察者 interface

namespace App;

interface LoginObserverInterface
{
    public function doNotify();
}
复制代码

发送邮件登录提醒类

class LoginEmailNotify implements LoginObserverInterface
{

    private $loginSubject;

    public function __construct(LoginSubjectInterface $loginSubject)
    {
        $this->loginSubject = $loginSubject;
        $this->loginSubject->attach($this);
    }

    public function doNotify()
    {
        echo '发送邮件登录提醒' . PHP_EOL;
    }
}
复制代码

发送短信通知类

class LoginDisable implements LoginObserverInterface
{
    
    private $loginSubject;
    
    public function __construct(LoginSubjectInterface $loginSubject)
    {
        $this->loginSubject = $loginSubject;
        $this->loginSubject->attach($this);
    }

    public function doNotify()
    {
        echo '微信推送充值连接' . PHP_EOL;
    }
}
复制代码

测试

$loginObject = new Login();
new LoginEmailNotify($loginObject);
new LoginPhoneMsgNotify($loginObject);

$result = $loginObject->handleLogin(['email' => '1350495180@qq.com', 'passwd' => '123456']);

output:
发送邮件登录提醒
发送短信通知
{"isLogin":false,"message":"帐号已经被禁用"}
复制代码

咱们发现了在观察者类中有一部分重复代码, 每一个观察者类中, 就是向被观察者业务类执行 attch 操做, 这部分能够抽出基类做为封装。另一点没实现的就是 doNotify 必须做为一个参数将当前登录的账号或手机号传递过去, 做为发送短信依据.

咱们大部分使用观察者都是使用推的模式, 被动接口 notify, 其实还有一种模式为 拉 模式, 其实核心就是在 notify 中返向调用符合自身业务的接口去处理本身的逻辑.

咱们尝试使用 SPL 来优化观察者

SPL提供了一组标准数据结构, 下面使用了观察者相关的 SplSubject、SplObserver两种接口使用方式

subject 业务主类

use SplObserver;
use SplObjectStorage;

class Login implements \SplSubject
{

    private $observers;

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

    /**
     * 加入观察者
     *
     * @param SplObserver $loginSubject
     */
    public function attach(SplObserver $loginObserver)
    {
        $this->observers->attach($loginObserver);
    }

    /**
     * 移除观察者
     *
     * @param SplObserver $loginObserver
     */
    public function detach(SplObserver $loginObserver)
    {
        $this->observers->detach($loginObserver);
    }

    /**
     * 通知观察者事件
     */
    public function notify()
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    /**
     * 处理登录
     *
     * @return array
     */
    public function handleLogin($param)
    {

        $isLogin = false;
        //执行登录
        switch ($this->doLogin($param)) {
            case 0:
                $message = '登录成功';
                $isLogin = true;
                break;
            case 1:
                $message = '账号或密码不对';
                break;
            case 2:
                $message = '帐号已经被禁用';
                break;
            default:
                $message = '登录失败';

        }

        $this->notify();

        return [
            'isLogin' => $isLogin,
            'message' => $message,
        ];
    }

    /**
     * 执行具体登录操做
     *
     * @return int
     */
    public function doLogin($param)
    {
        //dododo
        return rand(0, 2);
    }
}
复制代码

邮件通知类

use SplObserver;
use SplSubject;

class LoginEmailNotify implements SplObserver
{

    private $loginSubject;

    public function __construct(SplSubject $loginSubject)
    {
        $this->loginSubject = $loginSubject;
        $this->loginSubject->attach($this);
    }

    public function update(SplSubject $subject)
    {
        echo '发送邮件登录提醒' . PHP_EOL;
    }
}
复制代码

短信通知类

use SplObserver;
use SplSubject;

class LoginPhoneMsgNotify implements SplObserver
{

    private $loginSubject;
    
    public function __construct(SplSubject $loginSubject)
    {
        $this->loginSubject = $loginSubject;
        $this->loginSubject->attach($this);
    }
    
    public function update(SplSubject $subject)
    {
        echo '发送短信通知' . PHP_EOL;
    }
}
复制代码

以上代码咱们使用了php spl内部封装好的 SplSubject、SplObserver的接口, 以及 SplObjectStorage 对象存储类. 固然在方便的同时也带来了缺失部分灵活性, 例如通知观察者只能实现 update 类接口.

结论

小明和产品经理结局是?

相关文章
相关标签/搜索