提早了解设计模式的 六大原则
,它们代码演化的 目标 和 驱动力。php
需求:将网站的报错信息经过 Email 发送给管理员。程序员
需求背景:网站报500类错误时,管理员和开发人员并不能实时知道,等查看日志时或用户打电话过来返回问题时,有可能已经形成了极大的不良影响。so,开发一个实时通知功能,有问题早发现早治疗,岂不美哉?sql
心里想法:这简单,写个异常处理器,配置到系统中进行监听,渲染时走 Email 类发出去。一顿过程化编程操做猛如虎...数据库
// Email 类 class Email { // 发送 public function send(){} } // 错误异常处理器 class ErrorHandler { // 渲染异常和错误 protected function renderException() { $prepare['host'] = 'smtp.qq.com'; $prepare['port'] = 587; $prepare['encryption'] = 'tls'; $prepare['username'] = 'QQ 号'; $prepare['password'] = '受权码'; // ... 艰辛复杂的对象初始化工做 $Mailer = new Mail($prepare); // 设置发送信息 $Mailer->setFrom('noreply@domain.com') ->setTo('admin@domain.com') ->setSubject('报错了!') ->setTextBody('具体的报错内容'); // 嗖~,搞定! $Mailer->send(); } }
Gof: 嗯...
这段代码,很头疼,违反了...不少原则啊。
ErrorHandler
做为客户端,想发送邮件出去,却参与了邮件发送对象的初始化工做,身为客户端很累的,不符合单一职责原则
。
ErrorHandler
也知道了邮件发送对象的建立方法,可是它知道这个干嘛? 也不符合迪米特原则
。
可是,若是他们的产品人员提到需求“止步于此”的话,这样写也行。
若是开发人员“积极进取”的话,代码能够多往下走一个阶段,由于...太过程化编程了!编程
需求:有人退订单了或下单了长时间没有付款的客户,邮件通知到客服。设计模式
需求背景:让客服跟进一下异常订单的状况,知己知彼,百战不殆嘛。api
心里想法:这简单,写个订单监听器,配置到系统中进行监听,须要的时候走 EMail 类发出去,已经搞过报错信息通知,这个简单。又在另外一处一顿过程化编程操做猛如虎...dom
// Email 类 class Email { // 发送 public function send(){} } // 订单监听器 class OrderHandler { // 通知 protected function notifly() { $prepare['host'] = 'smtp.qq.com'; $prepare['port'] = 587; $prepare['encryption'] = 'tls'; $prepare['username'] = 'QQ 号'; $prepare['password'] = '受权码'; // ... 艰辛复杂的对象初始化工做 $Mailer = new Email($prepare); // 设置发送信息 $Mailer->setFrom('noreply@domain.com') ->setTo('service@domain.com') ->setSubject('有异常订单!') ->setTextBody('具体的订单信息'); // 发送,搞定! $Mailer->send(); } }
Gof:这就看不过过去了,可复用性太差了,更换个邮件配置还得改多处。须要优化一下了。
心里想法: OK,封装一下,封装到一处。性能
// EMail 类 class Email { private static $_instance = null; private function __construct(){} private function __clone(){} // 获取对象 public static function getInstance() { if( self::$_instance == null ){ $prepare['host'] = 'smtp.qq.com'; $prepare['port'] = 587; $prepare['encryption'] = 'tls'; $prepare['username'] = 'QQ 号'; $prepare['password'] = '受权码'; // ... 艰辛复杂的对象初始化工做 self::$_instance = new self($prepare); } return self::$_instance; } // 设置消息体 public function initMessage($title, $content) { // 设置消息 $this->setFrom('noreply@domain.com') ->setTo('service@domain.com') ->setSubject($title) ->setTextBody($content); } // 嗖~ public function send(){} } // 订单监听器 class OrderHandler { // 通知 protected function notifly() { $Mailer = Email::getInstance(); // 设置发送信息 $Mailer->initMessage('有异常订单!', '具体的订单信息'); // 嗖~ $Mailer->send(); } } // 错误异常处理器 class ErrorHandler { // 渲染异常和错误 protected function renderException() { $Mailer = Email::getInstance(); // 设置发送信息 $Mailer->initMessage('报错了!', '具体的报错内容'); // 嗖~ $Mailer->send(); } }
Gof:嗯,孺子可教也。
ErrorHandler
、OrderHandler
做为客户端,再也不关心邮件对象的建立过程,直接拿来就用,符合了单一职责原则
和迪米特原则
。
还将单例模式
,节省了内存空间,提升了性能,不错!
需求:切换发送通道,经过钉钉群消息发送,关闭原来的 Email 发送通道。测试
需求背景:邮件通知只通知了相应几个管理员,当有人员变化是还须要改收信人配置,最主要的是邮件提醒也不及时啊,有的人还懒的刷邮件。最近公司启用了钉钉,直接走钉钉群自定义消息,人员变更直接屏蔽在外部,增删群成员就行,消息收取方便了,谁看了过了也能知道,报错信息也从共有知识变成了公共知识,岂不美哉?
心里想法:处世多年的经验告诉我 Email
邮件通知类不能删除,万一哪天要再加上 Email 发送功能呢?说不定要通知的人不是内部人员没有钉钉帐号呢,或要求钉钉消息和邮件同时发送呢, 删除了还得重写。坚定不能删除。以前邮件类优化了一版,此次钉钉通知类直接一步到位,将实例化过程封装在本身内部。
// 钉钉通知类 class DingDing { private static $_instance = null; private function __construct(){} private function __clone(){} // 获取对象 public static function getInstance() { if( self::$_instance == null ){ $prepare['url'] = 'https://oapi.dingtalk.com/robot/send?access_token='; $prepare['token'] = '123456abcdefg'; // ... 艰辛复杂的对象初始化工做 self::$_instance = new self($prepare); } return self::$_instance; } // 设置消息体 public function initMessage($title, $content) { // 设置消息 $this->contentBody([ "msgtype" => "text", "text" => [ "content" => "{$title}\n {$content}", ], ]); } // 发送消息 public function sendMsg(){} } // 错误异常处理器 class ErrorHandler { // 渲染异常和错误 protected function renderException() { // 经过配置获取使用的消息通道 $channel = 'dingding'; switch ($channel) { case "dingding": $DingDing = DingDing::getInstance(); // 设置消息 $DingDing->initMessage('报错了!', '具体的报错内容'); // 发送,搞定! $DingDing ->sendMsg(); break; // case "other": //... case "email": default: $Mailer = Email::getInstance(); // 设置发送人 $Mailer->initMessage('报错了!', '具体的报错内容'); // 发送,搞定! $Mailer->send(); } } }
Gof:《从0到1》告诉咱们,0到1很难,1到n却很简单,需求也是这样。有2个通知类型很快就会有多个通知类,到时候renderException()
将会很臃肿。
并且,身为 高层模块 的ErrorHandler
异常处理器类直接依赖的 底层模块 的DingDing
钉钉通知类,也违背了依赖倒置原则
。
每次新增、修改通知类时都须要修改ErrorHandler
类,也不符合开发-封闭原则
。
ErrorHandler
类知道了全部的消息通知类
,可是它其实只要消息通知的功能而已,违背了迪米特原则
。
得改!
心里想法:
经过 依赖倒置原则
咱们将 依赖细节(类、对象)改成 依赖抽象(抽象类、接口),对消息通知类进行抽象出 消息通知接口。
抽象出接口后,就能够经过 实例化参数 或 方法参数 加上 接口类型 来限制只传入咱们须要的对象。不至于开发人员传递对象错误到运行时才报错的情况出现。
// 通知接口 interface INotify { // 获取实例 public function getInstance(); // 准备消息体 public function initMessage($title, $content); // 发送消息 public function send(); } // 钉钉类 class DingDing implements INotify { private $title; private $content; // 获取实例 public function getInstance(){return new self();} // 准备消息体 public function initMessage($title, $content){ $this->title = $title; $this->content = $content; } // 发送消息 public function send(){ echo $this->title. $this->content; } } // EMail 类 class Email implements INotify { private $title; private $content; // 获取实例 public function getInstance(){return new self();} // 准备消息体 public function initMessage($title, $content){ $this->title = $title; $this->content = $content; } // 发送消息 public function send(){ echo $this->title. $this->content; } } // 错误异常处理器 class ErrorHandler { // 消息通知对象 private $notify; // 初始化时限定传入符合 INotify 接口的类 public function __construct(INotify $notifyObj) { $this->notify = $notifyObj; } // 渲染异常和错误 public function renderException() { // 初始化消息体 $this->notify->initMessage('有报错了!', '具体的报错信息'); // 发送消息 $this->notify->send(); } } // 订单监听器 class OrderHandler { // 消息通知对象 private $notify; // 初始化时限定传入符合 INotify 接口的类 public function __construct(INotify $notifyObj) { $this->notify = $notifyObj; } // 通知 public function notify() { // 初始化消息体 $this->notify->initMessage('有异常订单!', '具体的订单信息'); // 发送消息 $this->notify->send(); } }
客户端代码
# 错误异常处理器客户端 // 经过配置获取使用的消息通道 $channelError = 'dingding'; switch ($channelError) { case "dingding": $MessageNotify = DingDing::getInstance(); break; case "other1": //... case "email": default: $MessageNotify = Email::getInstance(); } $ErrorHandler = new ErrorHandler($MessageNotify); $ErrorHandler->renderException(); # 订单监听器客户端 // 经过配置获取使用的消息通道 $channelOrder = 'email'; switch ($channelOrder) { case "dingding": $MessageNotify = DingDing::getInstance(); break; case "other1": //... case "email": default: $MessageNotify = Email::getInstance(); } $OrderHandler = new OrderHandler($MessageNotify); $OrderHandler->notify();
Gof:高层模块的
ErrorHandler
异常处理器依赖了抽象的INotify
接口,符合了依赖倒置原则
。
当有新的的消息通知需求时直接实现INotify
接口,并经过初始化参数
传入便可,不用再修改ErrorHandler
类,也符合了开发-封闭原则
。
经过初始化参数
传入具体的消息通知对象,ErrorHandler
也不用再关心具体有多少种通知方式,具体用的哪一种通知方式,也符合了迪米特原则
。可是还有如下不足,须要进步抽象优化。
- 具体建立哪一个消息通知对象的处理出现了重复,须要整合到一处,方便修改和复用。
- 消息通知对象的建立过程放到了消息通知类的内部,多少有点违背了
单一职责原则
。若是建立过程很“复杂”,强依赖了外部环境,例如依赖别的类的实例,或直接从具体的数据源(例如:DB,Redis)中读取配置等,将不利于之后的测试和功能迭代,应该将建立过程提到类外部,必要的参数经过初始化参数形式传入。
// 通知消息工厂 class NotifyFactory { // 建立通知消息对象 public static function create($channel) { switch ($channel) { case "dingding": $MessageNotify = DingDing::getInstance(); break; case "other1": //... case "email": default: $MessageNotify = Email::getInstance(); } return $MessageNotify; } } # 错误异常处理器客户端 // 经过配置获取使用的消息通道 $channelError = 'dingding'; $MessageNotify = NotifyFactory::create($channelError); $ErrorHandler = new ErrorHandler($MessageNotify); $ErrorHandler->renderException(); # 订单监听器客户端 // 经过配置获取使用的消息通道 $channelOrder = 'email'; $MessageNotify = NotifyFactory::create($channelOrder); $OrderHandler = new OrderHandler($MessageNotify); $OrderHandler->notify();
等等灯等灯: 如今已经达成了 简单工厂模式
。
Gof: 嗯不错,消息通知类对外部的依赖被提到了类的外部,这样的好处多啊:
- 方便对类进行自动化测试,例如
DingDing
原来初始化时从DB
中获取配置进行初始化。单独测试DingDing
类时,就必需要连上DB
数据库,如今须要将配置经过初始化参数传入接口,参数值想从哪取就从哪取。- 外部依赖变动时,只须要对
NotifyFactory
类进行修改,修改影响范围变小了。不过,还有一个问题,那就是每次新增消息通知类时都须要修改
NotifyFactory
消息通知工厂的代码,往里添加case
判断,不符合开发-封闭原则
。
心里想法:咱们能够进一步抽象,将对代码的修改调整为对类的增删上。
// 通知消息工厂接口 interface IFactory { // 建立通知消息对象 public function create(); } // 钉钉工厂类 class DingDingFactory implements IFactory { public function create() { return DingDing::getInstance(); } } // Email工厂类 class EmailFactory implements IFactory { public function create() { return Email::getInstance(); } } // Sms 工厂类 class SmsFactory implements IFactory { public function create() { return Sms::getInstance(); } } // Sms 类 class Sms implements INotify { private $title; private $content; // 获取实例 public function getInstance(){return new self();} // 准备消息体 public function initMessage($title, $content){ $this->title = $title; $this->content = $content; } // 发送消息 public function send(){ echo $this->title. $this->content; } } # 错误异常处理器客户端 // 经过配置获取消息通知工厂类名 $classError = 'DingDingFactory'; // $classError = 'SmsFactory'; 新增预备的短信通知通道 $MessageNotify = (new $classError())->create(); $ErrorHandler = new ErrorHandler($MessageNotify); $ErrorHandler->renderException(); # 订单监听器客户端 // 经过配置获取消息通知工厂类名 $channelOrder = 'EmailFactory'; $MessageNotify = (new $channelOrder())->create(); $OrderHandler = new OrderHandler($MessageNotify); $OrderHandler->notify();
等等灯等灯: 如今已经达成了 工厂方法模式
。
Gof: 当新增消息通知类
时,已不须要修改任何已有代码,只须要新增一个通知工厂类
和一个消息通知类
便可。
完美!!!
此时启用消息通知类的变动被限定在配置文件或数据库数据配置变化上,切换消息通知通道并不须要修改程序代码。
需求:发送通知时记录一下日志,方便往后查询与统计。
需求背景:有了即时通知,但想后期查询或统计怎么办,记录一下日志吧。异常订单比较重要,记录到 MySQL
中,查询异常报错字段比较多,记录的 Elasticsearch
中。
心里想法:日志类和消息通知类很像嘛,直接搞成工厂方法模式,哈哈。
// 日志接口 interface ILog { // 获取实例 public function getInstance(); // 写日志 public function write(); } // Mysql Log 类 class MysqlLog implements ILog {} // EalsticsearchLog 类 class EalsticsearchLog implements ILog {} // 日志工厂接口 interface ILogFactory { // 建立日志记录对象 public function create(); } // Mysql 日志工厂类 class MysqlLogFactory implements ILogFactory { public function create() { return MysqlLog::getInstance(); } } // Ealsticsearch 日志工厂类 class EalsticsearchLogFactory implements ILogFactory { public function create() { return EalsticsearchLog::getInstance(); } } # 错误异常处理器客户端 // 经过配置获取消息通知工厂类名 $classError = 'DingDingFactory'; $MessageNotify = (new $classError())->create(); // 经过配置获取日志记录工厂类名 $logClassError = 'EalsticsearchLogFactory'; $Log = (new $logClassError)->create(); $ErrorHandler = new ErrorHandler($MessageNotify, $Log); $ErrorHandler->renderException(); # 订单监听器客户端 // 经过配置获取消息通知工厂类名 $channelOrder = 'EmailFactory'; $MessageNotify = (new $channelOrder())->create(); // 经过配置获取日志记录工厂类名 $logClassError = 'MysqlLogFactory'; $Log = (new $logClassError)->create(); $OrderHandler = new OrderHandler($MessageNotify, $Log); $OrderHandler->notify();
Gof: 新增一个工厂模式并不是这么简单就能解决问题的。 目前需求是 消息通知 并 记录日志 ,二者已是组合
关系,将这种组合关系下放到客户进行建立那么就不符合单一职责原则
。
客户端还知道有2个工厂,2个工厂生产的东西必须搭配在一块儿才能实现消息通知并记录日志的功能,不符合迪米特原则
。
相似买苹果笔记本,有不一样的配置,简单那2个配置举例子,cpu 分为 i7 和 i5, 屏幕分为 13 寸 和 15 寸。
普通消费者买笔记本会说:“我要个玩游戏爽的笔记本”,店员应该直接给出 i7 + 15 寸配置的机型。
若是一我的也是玩游戏,过来直接说:“要个 i7 + 15 寸配置的机型”,那这我的一听就是程序员(知道的太多了!不符合单一职责原则
和迪米特原则
)。
// 通知消息工厂接口 interface IFactory { // 建立通知消息对象 public function createNotify(); // 建立日志记录对象 public function createLog(); } // 异常报错工厂类 class ErrorFactory implements IFactory { public function createNotify() { return DingDing::getInstance(); } public function createLog() { return EalsticsearchLog::getInstance(); } } // 异常订单工厂类 class OrderFactory implements IFactory { public function createNotify() { return Email::getInstance(); } public function createLog() { return MysqlLog::getInstance(); } } # 错误异常处理器客户端 // 经过配置获取异常错误工厂类名 $classError = 'ErrorFactory'; $Factory = new $classError(); $ErrorHandler = new ErrorHandler($Factory->createNotify(), $Factory->createLog()); $ErrorHandler->renderException(); # 订单监听器客户端 // 经过配置获取异常订单工厂类名 $classError = 'OrderFactory'; $Factory = new $classError();; $OrderHandler = new OrderHandler($Factory->createNotify(), $Factory->createLog()); $OrderHandler->notify();
等等灯等灯: 如今已经达成了 抽象工厂方法模式
。