设计模式是对某些典型易变问题的特定解决方案,这些问题和解决方案经过分类总结,并且为了方便交流给每个解决方案都起了特定的名字。
模式是为了解决变化的问题,将变化的问题进行封装,让变化单独变化而互不影响,以达到项目系统的扩展性,模式是一种更高层次的代码复用。
设想一下,假如我们的项目是一次性项目,一旦发布,以后再也不会修改,那么我们完全没有必要使用设计模式,反而使用了设计模式会增加系统的复杂度,但是一次性的项目在我们实际工作环境中是极少的,如果实际工作环境中都是一次项目,那个时候我们开发人员都应该转行了。
每一个设计模式都是为了解决一类特定的问题而存在,是一种更高层级的复用
每一个设计模式都有一个特定的名字,团队协作中,既可以减少沟通成本,同时也可以保证项目质量
模式有助于开发人员提高解决问题的思考层次,能够让我们的系统更容易扩展
前辈们经过长期经验积累总结了许多设计模式,如单例模式、工厂模式、享元模式、策略模式、观察者模式等等,这些模式都是为了解决某一类特定的问题而设计,但是我们在日常编码过程中可以把这些模式作为一种参考即可,我们在编码过程中满足模式的原则即认为是好的代码。
模式有以下六大原则
名称 | 解释 |
---|---|
单一职责 | 要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一项职责 |
里式代换 | 子类可以扩展父类的功能,但不能改变父类原有的功能 |
依赖倒置 | 高层模块不应该依赖低层模块,二者都应该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象 |
接口隔离 | 客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上 |
迪米特法则 | 一个对象应该对其他对象保持最少的了解 |
开闭原则 | 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭 |
单一职责看似简单,其实并不简单,这个原则告诉了我们划分一个类功能的标准,下面我们看一段常见的代码片段
/**
* Class Login
* @datetime 2020/7/12 4:13 PM
* @author roach
* @email [email protected]
*/
abstract class Login
{
/**微信appId
* @var string
* @datetime 2020/7/12 4:11 PM
* @author roach
* @email [email protected]
*/
public $wxAppid;
/**微信密钥
* @var string
* @datetime 2020/7/12 4:11 PM
* @author roach
* @email [email protected]
*/
public $wxAppSecret;
/**微信登录
* @return mixed
* @datetime 2020/7/12 4:12 PM
* @author roach
* @email [email protected]
*/
abstract public function wxLogin();
/**手机号验证码登录
* @return mixed
* @datetime 2020/7/12 4:12 PM
* @author roach
* @email [email protected]
*/
abstract public function phoneLogin();
}
以上例程代码是一个用户登录的代码片段,为了演示方便作者把
wxLogin
和phoneLogin
两个方法实现细节进行了屏蔽。
以上代码有什么问题吗?不都是这么做登录的吗?
从模式原则来分析,假如微信登录实现细节发生修改,我们要修改
Login
类,手机号验证码登录实现细节发生修改,我们同样要修改Login
类,如果我们要增加一种QQ授权登录方式,同样我们要修改Login
类
在模式的理论中,只要发生修改就会有风险,那么我们怎么解决这个问题呢?按照单一职责原则,作者把代码做了如下拆分优化
/**
* Class Login
* @datetime 2020/7/12 4:13 PM
* @author roach
* @email [email protected]
*/
abstract class WxLogin
{
/**微信appId
* @var string
* @datetime 2020/7/12 4:11 PM
* @author roach
* @email [email protected]
*/
public $wxAppid;
/**微信密钥
* @var string
* @datetime 2020/7/12 4:11 PM
* @author roach
* @email [email protected]
*/
public $wxAppSecret;
/**
* @param array $params
* @return mixed
* @datetime 2020/7/12 4:20 PM
* @author roach
* @email [email protected]
*/
abstract public function login($params = []);
}
/**
* Class PhoneLogin
* @datetime 2020/7/12 4:20 PM
* @author roach
* @email [email protected]
*/
abstract class PhoneLogin
{
/**
* @param array $params
* @return mixed
* @datetime 2020/7/12 4:20 PM
* @author roach
* @email [email protected]
*/
abstract public function login($params = []);
}
作者将
Login
类一分为二,微信登录的appId
等依赖也放到各自类中,这样调用端如果想实现登录功能选择调用类就可以了。
现在我们分析,假如微信登录实现要发生修改,我们直接修改
WxLogin
类就可以了,同样的,假如手机号验证码登录实现要发生修改,我们只需要修改PhoneLogin
类就可以了;假如我们要增加一种QQ授权登录的方式,此时我们增加一个QqLogin
类就可以了。
里式代换原则强调,我们能够扩展基类的原有功能,但不能改变基类原有的功能,这样很容易出现问题,下面还是看一段示例代码
/**
* Class Model
* @datetime 2020/7/12 4:30 PM
* @author roach
* @email [email protected]
*/
class Model
{
/**
* @var \PDO
* @datetime 2020/7/12 4:29 PM
* @author roach
* @email [email protected]
*/
private $_db;
/**
* @param \PDO $db
* @datetime 2020/7/12 4:45 PM
* @author roach
* @email [email protected]
*/
public function setDb(\PDO $db)
{
$this->_db = $db;
}
/**
* @return \PDO
* @datetime 2020/7/12 4:32 PM
* @author roach
* @email [email protected]
*/
public function getDb()
{
if(is_null($this->_db)) {
$this->setDb(new \PDO('mysql:host=127.0.0.1;port=3306;dbname=roach', 'roach', 'roach'));
}
return $this->_db;
}
/**
* @param null $name
* @return string
* @datetime 2020/7/12 4:36 PM
* @author roach
* @email [email protected]
*/
public function lastInsertId($name = null)
{
return $this->_db->lastInsertId($name);
}
}
/**
* Class UserModel
* @datetime 2020/7/12 4:35 PM
* @author roach
* @email [email protected]
*/
class UserModel extends Model
{
/**
* @return PDO|string
* @datetime 2020/7/12 4:35 PM
* @author roach
* @email [email protected]
*/
public function getDb()
{
return MyModel::db();
}
}
/**
* Class MyModel
* @datetime 2020/7/12 4:34 PM
* @author roach
* @email [email protected]
*/
class MyModel
{
/**
* @var \PDO
* @datetime 2020/7/12 4:33 PM
* @author roach
* @email [email protected]
*/
private static $_db;
/**
* @return PDO
* @datetime 2020/7/12 4:34 PM
* @author roach
* @email [email protected]
*/
public static function db()
{
if(is_null(self::$_db)) {
self::$_db = new \PDO('mysql:host=127.0.0.1;port=3306;dbname=roach', 'roach', 'roach');
}
return self::$_db;
}
}
以上代码也是真实遇到的,分析以上代码,在多人并行开发的团队,一个开发发现
Model
类的db
并非真正的单例,于是这个开发在不改变业务调用代码的基础上,增加了MyModel
类实现了约定单例,但是在实现UserModel
类getDb
方法的时候,改变了基类Model
原有的getDb
方法给_db
私有属性赋值,这样导致基类lastInsertId
方法不能使用,此时如果我们使用基类可以正常调用lastInsertId
方法,但是使用子类UserModel
调用lastInsertId
就会发生异常。
以上代码违背了里式代换原则,那么我们应该如何修改呢?作者对
UserModel
进行了如下优化
/**
* Class UserModel
* @datetime 2020/7/12 4:35 PM
* @author roach
* @email [email protected]
*/
class UserModel extends Model
{
/**
* @return \PDO
* @datetime 2020/7/12 4:35 PM
* @author roach
* @email [email protected]
*/
public function getDb()
{
$this->setDb(MyModel::db());
return parent::getDb();
}
}
这样修改后保证了单例,和里式代换
依赖倒置的思想是个很经典扩展性思维,开发人员如果能够深刻理解依赖倒置原则,将有很大层次的提升,还是看如下代码
/**
* Class FileLog
* @datetime 2020/7/12 6:09 PM
* @author roach
* @email [email protected]
*/
class FileLog
{
/**
* @var string
* @datetime 2020/7/12 6:05 PM
* @author roach
* @email [email protected]
*/
protected $_logFile;
/**
* @param $fileName
* @datetime 2020/7/12 6:06 PM
* @author roach
* @email [email protected]
*/
public function setLogFile($fileName)
{
$this->_logFile = $fileName;
}
/**
* @param string $message
* @param array $context
* @param string $leftPlace
* @param string $rightPlace
* @return string
* @datetime 2019/8/30 18:00
* @author roach
* @email [email protected]
*/
static public function interpolate($message, array $context = [], $leftPlace='{', $rightPlace='}')
{
if(empty($context)) {
return $message;
}
$replace = [];
foreach ($context as $key => $val) {
$replace[$leftPlace . $key . $rightPlace] = $val;
}
return strtr($message, $replace);
}
/**
* @param string $level
* @param string $message
* @param array $params
* @datetime 2020/7/12 6:07 PM
* @author roach
* @email [email protected]
*/
public function log($level, $message, $params = [])
{
$message = self::interpolate($message, $params);
$message = json_encode([
'datetime' => date('Y-m-d H:i:s'),
'level' => $level,
'msg' => $message,
'clientIp' => $_SERVER['REMOTE_ADDR'],
'url' => $_SERVER['REQUEST_URI'],
'method' => $_SERVER['REQUEST_METHOD'],
'host' => $_SERVER['HTTP_HOST']
], JSON_UNESCAPED_UNICODE);
file_put_contents($this->_logFile, $message.PHP_EOL, FILE_APPEND | LOCK_EX);
}
}
/**
* Class DbLog
* @datetime 2020/7/12 6:11 PM
* @author roach
* @email [email protected]
*/
class DbLog
{
/**
* @var \PDO
* @datetime 2020/7/12 6:11 PM
* @author roach
* @email [email protected]
*/
protected $_pdo;
/**
* @param \PDO $db
* @datetime 2020/7/12 6:12 PM
* @author roach
* @email [email protected]
*/
public function setDb(\PDO $db)
{
$this->_pdo = $db;
}
/**
* @param string $message
* @param array $context
* @param string $leftPlace
* @param string $rightPlace
* @return string
* @datetime 2019/8/30 18:00
* @author roach
* @email [email protected]
*/
static public function interpolate($message, array $context = [], $leftPlace='{', $rightPlace='}')
{
if(empty($context)) {
return $message;
}
$replace = [];
foreach ($context as $key => $val) {
$replace[$leftPlace . $key . $rightPlace] = $val;
}
return strtr($message, $replace);
}
/**
* @param string $level
* @param string $message
* @param array $params
* @return int
* @datetime 2020/7/12 6:16 PM
* @author roach
* @email [email protected]
*/
public function log($level, $message, $params = [])
{
$message = self::interpolate($message, $params);
$stmt = $this->_pdo->prepare('INSERT INTO `logs`(`datetime`, `level`, `msg`, `clientIp`, `url`, `method`, `host`)VALUES(?,?,?,?,?,?,?)');
$stmt->execute([
date('Y-m-d H:i:s'),
$level,
$message,
$_SERVER['REMOTE_ADDR'],
$_SERVER['REQUEST_URI'],
$_SERVER['REQUEST_METHOD'],
$_SERVER['HTTP_HOST']
]);
return $stmt->rowCount();
}
}
/**
* Class LogFactory
* @datetime 2020/7/12 6:21 PM
* @author roach
* @email [email protected]
*/
class Logger
{
/**
* @param string $loggerName
* @param string $level
* @param string $message
* @param array $params
* @return int|void
* @datetime 2020/7/12 6:25 PM
* @author roach
* @email [email protected]
*/
public function log($loggerName, $level, $message, $params = [])
{
if($loggerName === 'db') {
$logger = new DbLog();
$logger->setDb(new \PDO('mysql:host=127.0.0.1;port=3306;dbname=roach', 'roach', 'roach'));
return $logger->log($level, $message, $params);
}
$logger = new FileLog();
$logger->setLogFile('/tmp/logs/'.date('Y').'/'.date('m-d').'.log');
return $logger->log($level, $message, $params);
}
}
以上代码示例中,
Logger
类的log
方法通过传入参数$loggerName
来决定用哪个类去记录日志,这样Logger
类直接依赖FileLog
和DbLog
类,当我们再增加一个KafkaLog
类打日志时,Logger
同样需要依赖KafkaLog
类,这样Logger
类就需要发生修改,在模式领域,这样的代码认为不可以扩展,那么怎么优化呢?
/**
* Class ILog
* @datetime 2020/7/12 6:32 PM
* @author roach
* @email [email protected]
*/
abstract class ILog
{
/**
* @param string $message
* @param array $context
* @param string $leftPlace
* @param string $rightPlace
* @return string
* @datetime 2019/8/30 18:00
* @author roach
* @email [email protected]
*/
static public function interpolate($message, array $context = [], $leftPlace='{', $rightPlace='}')
{
if(empty($context)) {
return $message;
}
$replace = [];
foreach ($context as $key => $val) {
$replace[$leftPlace . $key . $rightPlace] = $val;
}
return strtr($message, $replace);
}
/**
* @param string $level
* @param string $message
* @param array $params
* @return mixed
* @datetime 2020/7/12 6:30 PM
* @author roach
* @email [email protected]
*/
abstract public function log($level, $message, $params = []);
}
/**
* Class FileLog
* @datetime 2020/7/12 6:09 PM
* @author roach
* @email [email protected]
*/
class FileLog extends ILog
{
/**
* @var string
* @datetime 2020/7/12 6:05 PM
* @author roach
* @email [email protected]
*/
protected $_logFile;
/**
* @param $fileName
* @datetime 2020/7/12 6:06 PM
* @author roach
* @email [email protected]
*/
public function setLogFile($fileName)
{
$this->_logFile = $fileName;
}
/**
* @param string $level
* @param string $message
* @param array $params
* @datetime 2020/7/12 6:07 PM
* @author roach
* @email [email protected]
*/
public function log($level, $message, $params = [])
{
$message = self::interpolate($message, $params);
$message = json_encode([
'datetime' => date('Y-m-d H:i:s'),
'level' => $level,
'msg' => $message,
'clientIp' => $_SERVER['REMOTE_ADDR'],
'url' => $_SERVER['REQUEST_URI'],
'method' => $_SERVER['REQUEST_METHOD'],
'host' => $_SERVER['HTTP_HOST']
], JSON_UNESCAPED_UNICODE);
file_put_contents($this->_logFile, $message.PHP_EOL, FILE_APPEND | LOCK_EX);
}
}
/**
* Class DbLog
* @datetime 2020/7/12 6:11 PM
* @author roach
* @email [email protected]
*/
class DbLog extends ILog
{
/**
* @var \PDO
* @datetime 2020/7/12 6:11 PM
* @author roach
* @email [email protected]
*/
protected $_pdo;
/**
* @param \PDO $db
* @datetime 2020/7/12 6:12 PM
* @author roach
* @email [email protected]
*/
public function setDb(\PDO $db)
{
$this->_pdo = $db;
}
/**
* @param string $level
* @param string $message
* @param array $params
* @return int
* @datetime 2020/7/12 6:16 PM
* @author roach
* @email [email protected]
*/
public function log($level, $message, $params = [])
{
$message = self::interpolate($message, $params);
$stmt = $this->_pdo->prepare('INSERT INTO `logs`(`datetime`, `level`, `msg`, `clientIp`, `url`, `method`, `host`)VALUES(?,?,?,?,?,?,?)');
$stmt->execute([
date('Y-m-d H:i:s'),
$level,
$message,
$_SERVER['REMOTE_ADDR'],
$_SERVER['REQUEST_URI'],
$_SERVER['REQUEST_METHOD'],
$_SERVER['HTTP_HOST']
]);
return $stmt->rowCount();
}
}
/**
* Class LogFactory
* @datetime 2020/7/12 6:21 PM
* @author roach
* @email [email protected]
*/
class Logger
{
/**
* @param ILog $logger
* @param string $level
* @param string $message
* @param array $params
* @return mixed
* @datetime 2020/7/12 6:32 PM
* @author roach
* @email [email protected]
*/
public function log(ILog $logger, $level, $message, $params = [])
{
return $logger->log($level, $message, $params);
}
}
我们可以看到,在优化过程中,我们增加了
ILog
抽象类,并增加了抽象方法log
,Logger
类中log
方法不再依赖$loggerName
,改为依赖抽象类ILog
,这样,当我们增加KafkaLog
类打日志时,Logger
类不需要再做任何修改。
这就是典型的依赖倒置思想。
我们刚刚学会了依赖倒置,高层模块不应该依赖于低层模块儿,二者应该依赖其抽象。接口隔离告诉我们,这个抽象要有个度,应该建立在最小接口上。
看如下代码
/**
* Interface IOrderAndLogin
* @datetime 2020/7/12 6:45 PM
* @author roach
* @email [email protected]
*/
interface IOrderAndLogin
{
/**
* @param array $params
* @return mixed
* @datetime 2020/7/12 6:44 PM
* @author roach
* @email [email protected]
*/
public function order($params = []);
/**
* @param array $params
* @return mixed
* @datetime 2020/7/12 6:46 PM
* @author roach
* @email [email protected]
*/
public function login($params = []);
}
/**
* Class ProductOrder
* @datetime 2020/7/12 6:48 PM
* @author roach
* @email [email protected]
*/
class ProductOrder implements IOrderAndLogin
{
/**
* Order constructor.
* @param array $params
*/
public function order($params = [])
{
//各种下单操作
return true;
}
/**
* @param array $params
* @return bool|mixed
* @datetime 2020/7/12 6:48 PM
* @author roach
* @email [email protected]
*/
public function login($params = [])
{
return false;
}
}
/**
* Class WxLogin
* @datetime 2020/7/12 6:50 PM
* @author roach
* @email [email protected]
*/
class WxLogin implements IOrderAndLogin
{
/**
* @param array $params
* @return bool|mixed
* @datetime 2020/7/12 6:49 PM
* @author roach
* @email [email protected]
*/
public function order($params = [])
{
return false;
}
/**
* @param array $params
* @return array|mixed
* @datetime 2020/7/12 6:50 PM
* @author roach
* @email [email protected]
*/
public function login($params = [])
{
// 各种操作
return [
'userId' => time(),
'nickname' => uniqid()
];
}
}
/**
* Class OrderController
* @datetime 2020/7/12 6:55 PM
* @author roach
* @email [email protected]
*/
class OrderController
{
/**
* @param IOrderAndLogin $order
* @return mixed
* @datetime 2020/7/12 6:55 PM
* @author roach
* @email [email protected]
*/
public function orderAction(IOrderAndLogin $order)
{
return $order->order([
'time' => time(),
]);
}
}
以上代码我们可以看到,
OrderController
通过接口IOrderAndLogin
依赖ProductOrder
,但是IOrderAndLogin
接口还有一个OrderController
用不到的接口方法login
,同时ProductOrder
类根本不需要login
方法,同样的,WxLogin
类也不需要order
方法,但是他们各自都必须去实现不需要的方法,这就不符合接口的单一职责原则。
这种优化思路很简单,把
IOrderAndLogin
接口拆分成IOrder
和ILogin
两个接口,ProductOrder
类实现IOrder
接口,WxLogin
类实现ILogin
接口,OrderController
类依赖IOrder
接口即可,由于代码很简单,不在提供实现细节。
迪米特法则简单来说就是高内聚、低耦合,一个对象应该对其他对象保持最少的了解。
从耦合度来讲,
继承>依赖
,所以当我们使用继承的时候,除非是有明确的父子关系,否则不要乱用继承。
下面举一个简单的高内聚、低耦合的例子
/**
* Created by PhpStorm.
* User: Jiang Haiqiang
* Date: 2020/7/12
* Time: 4:09 PM
*/
class RedisCache
{
/**
* @var string
* @datetime 2020/7/12 7:10 PM
* @author roach
* @email [email protected]
*/
public $host;
/**
* @var int
* @datetime 2020/7/12 7:10 PM
* @author roach
* @email [email protected]
*/
public $port;
/**
* @var string
* @datetime 2020/7/12 7:10 PM
* @author roach
* @email [email protected]
*/
public $password;
/**
* @var int
* @datetime 2020/7/12 7:10 PM
* @author roach
* @email [email protected]
*/
public $db;
/**
* @var \Redis
* @datetime 2020/7/12 7:11 PM
* @author roach
* @email [email protected]
*/
private $_instance;
/**
* @return \Redis
* @datetime 2020/7/12 7:13 PM
* @author roach
* @email [email protected]
*/
public function getRedis()
{
if(is_null($this->_instance)) {
$redis = new \Redis();
$redis->connect($this->host, $this->port);
$redis->auth($this->password);
$redis->select($this->db);
$this->_instance = $redis;
}
return $this->_instance;
}
/**
* @param string $key
* @return bool|string
* @datetime 2020/7/12 7:14 PM
* @author roach
* @email [email protected]
*/
public function get($key)
{
return $this->getRedis()->get($key);
}
/**
* @param string $key
* @param string $value
* @param int $timeout
* @return bool
* @datetime 2020/7/12 7:14 PM
* @author roach
* @email [email protected]
*/
public function set($key, $value, $timeout = 60)
{
return $this->getRedis()->set($key, $value, $timeout);
}
}
以上
RedisCache
封装类,也许大家在自己的项目中肯定看到过类似的代码,大家会觉得这有什么问题吗?
其实从高内聚低耦合的角度来讲,
host
,port
,password
、db
属性以及getRedis
方法,对于其他类来说根本不需要知道,本着高内聚低耦合的思想,这些的访问权限至少要控制在protected
级别,这样从技术角度避免了泄露和被修改的可能性。
开闭原则简单来说就是对修改关闭,对扩展开放,此原则理解起来稍复杂,我们可以狭义的这样理解。
如果我们的系统软件已经上线运行了,当发生新需求变化时,我们是通过增加代码实现来满足新需求而不是修改原来的代码来实现新需求,当然这是一个狭义的理解。
我们回头看依赖倒置的例子就是一个符合开闭原则的典型案例,当我们增加
KafkaLog
需求时,我们是通过增加KafkaLog
类实现来满足需求,原有的FileLog
类和DbLog
类没有发生任何更改,这样就是一个典型的对扩展开放,对修改关闭的一个例子。
好了,设计模式的六大原则我们都通过案例学习完了,你都学会了吗?