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

  • 2019 年 11 月 28 日
  • 筆記

前言

設計模式停更了好久, 發現兜兜轉轉回來, 還是離開不了那些個套路.

今天我們主要講解下 觀察者模式, 可能你聽這個名字感覺很熟, 如果給你說下還可以稱它為 發布訂閱 模式的話, 相信你對它就毫不陌生了.

觀察者模式定義了一種一對多的依賴關係, 讓多個觀察者對象同時監聽某一個主題對象, 這個主題發生變化時, 通知所有的觀察對象. 總的來說, 觀察者是解除耦合的重要手段.

V1 這個需求很簡單, 怎麼實現他不管

叮叮叮..

  • 產品經理: 我需要實現一個用戶登陸的介面, 這個介面很重要也很簡單, 趕緊實現下, 上去一梭子搞完, 節前就上線.
  • 小明: 好的, 經理

登陸程式碼:

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'=>'[email protected]','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' => '[email protected]', '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 類介面.

結論

小明和產品經理結局是?