共计 10628 个字符,预计需要花费 27 分钟才能阅读完成。
阅读须知
提前了解设计模式的 六大原则,它们代码演化的 目标 和 驱动力。
初始阶段
需求:将网站的报错信息通过 Email 发送给管理员。
需求背景:网站报 500 类错误时,管理员和开发人员并不能实时知道,等查看日志时或用户打电话过来返回问题时,有可能已经造成了极大的不良影响。so,开发一个实时通知功能,有问题早发现早治疗,岂不美哉?
内心想法:这简单,写个异常处理器,配置到系统中进行监听,渲染时走 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 也知道了邮件发送对象的创建方法,但是它知道这个干吗?也不符合迪米特原则。
但是,如果他们的产品人员提到需求“止步于此”的话,这样写也行。如果开发人员“积极进取”的话,代码可以多往下走一个阶段,因为 … 太过程化编程了!
第二阶段
需求:有人退订单了或下单了长时间没有付款的客户,邮件通知到客服。
需求背景:让客服跟进一下异常订单的情况,知己知彼,百战不殆嘛。
内心想法:这简单,写个订单监听器,配置到系统中进行监听,需要的时候走 EMail 类发出去,已经搞过报错信息通知,这个简单。又在另一处一顿过程化编程操作猛如虎 …
// 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 邮件通知类不能删除,万一哪天要再加上 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 异常处理器类直接依赖的 底层模块 的 Email 邮件通知类 和 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();
等等灯等灯:现在已经达成了 抽象工厂方法模式。