設計模式第八講-觀察者模式

前言

設計模式停更了很久, 發現兜兜轉轉回來, 仍是離開不了那些個套路.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 類接口.

結論

小明和產品經理結局是?

相關文章
相關標籤/搜索