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

  • 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 类接口.

结论

小明和产品经理结局是?