提早了解設計模式的 六大原則
,它們代碼演化的 目標 和 驅動力。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();
等等燈等燈: 如今已經達成了 抽象工廠方法模式
。