设计模式虽多,但实际开发中,我们常用的无非是简单的几种,即:工厂模式、依赖倒置、控制反转、依赖注入
简单工厂模式
简单工厂模式是编程中用到的最初级的设计模式,主要作用是分离代码中重复的建立对象的过程,实现代码的复用。
比如,当我们需要项目中根据需求不同,要链接不同类型的数据库,以下是一个factory.php代码
class Db{
public function getDb($db_type){
if($db_type == 'mongodb'){
return new MongoDB(...);
}elseif($db_type == 'redis'){
return new Redis(...);
}else{
return new Mysql(...);
}
}
}
//使用
$db_factory = new Db();
$db = $db_factory->getDb($_GET['type']);
以上只能算式简单工厂模式,还不够完善,下面我们将通引入依赖导致原则,使工厂模式更加完善
依赖倒置
依赖倒置是一种面向对象的设计原则。它要求高层调用和底层实现之间,不能直接依赖,应该依赖于抽象
简单工厂模式虽然规范了类的获取方式,但没有规范类的实现方式。
class MongoDb{
public function getOne(){}//获取1条数据
public function getList(){}//获取多条数据
}
class Redis{
public function getRow(){}//获取1条数据
public function getRows(){}//获取多条数据
}
class Mysql{
public function findOne(){}//获取1条数据
public function findAll(){}//获取多条数据
}
//使用
$db_factory = new Db();
$db = $db_factory->getDb($_GET['type']);
//获取一条数据
if($db_type == 'mongodb'){
$res = $db->getOne();
}elseif($db_type == 'redis'){
$res = $db->getRow();
}else{
$res = $db->findOne();
}
上面代码就是类的实现没有规范导致的结果,不同的数据库实例,对同一需求的操作方法不同,导致我们还要进行二次判断。
我们可以把判断的代码放在工厂类中,在类生成后,根据传入的操作参数来调用类中对应方法。但当需求增加时,需要添加判断代码,导致代码也来越臃肿。
此时依赖倒置就派上用场了。在上面的代码中,工厂类依赖了各个数据库操作类。依赖倒置就是把两者的依赖关系拆开,在他们中间加一层抽象类作为规范。
修改前依赖关系:工厂类–>数据库类
修改后依赖关系:工厂类–>抽象类/接口<–数据库类
数据库类从之前的被依赖类,变成了需要依赖其他类。依赖关系反转了,这就是依赖倒置。
修改后代码如下
//定义一个接口
interface DbInterface{
public function findOne();
public function findAll();
}
//所有数据库类必须实现DbInterface接口
class MongoDb implement DbInterface{
public function findOne(){}//获取1条数据
public function findAll(){}//获取多条数据
}
class Redis implement DbInterface{
public function findOne(){}//获取1条数据
public function findAll(){}//获取多条数据
}
class Mysql implement DbInterface{
public function findOne(){}//获取1条数据
public function findAll(){}//获取多条数据
}
//工厂类
class Db{
private DbInterface $model;
public function getDb($db_type):DbInterface{
if($db_type == 'mongodb'){
$this->model = new MongoDB(...);
}elseif($db_type == 'redis'){
$this->model = new Redis(...);
}else{
$this->model = new Mysql(...);
}
return $this->model;
}
}
工厂方法模式
简单工厂模式的缺点是,无法扩展。比如当需要增加orcal数据操作时,需要修改工厂类方法,违背了面向对象设计的开闭原则。
这个时候工厂方法模式可以弥补这个不足,代码如下
//定义一个接口
interface DbInterface{
public function findOne();
public function findAll();
}
//所有数据库类必须实现DbInterface接口
class MongoDb implements DbInterface{...}
class Redis implements DbInterface{...}
class Mysql implements DbInterface{...}
//工厂方法接口
interface getDb{
public static function create():DbInterface
}
//具体工厂类,创建一个具体产品实例
class MysqlDb implements getDb(){
public static function create(){
new Mysql();
}
}
//调用
$db = MysqlDb::create();
当需要新增orcal操作时,只需新增一个实现DbInterface的类和实现getDb的类即可,不需要修改工厂类代码。在调用层可自由实例化想要的数据库
所以工厂方法模的定义为
一个抽象产品类,可以派生出多个具体产品类。每个具体工厂类只能创建一个具体产品类的实例
抽象工厂模式
理解了工厂方法,抽象工厂其实就好理解了,它的定义如下
多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂类可以派生出多个具体工厂类。每个具体工厂类可以创建多个具体产品的实例。
说人话就是,抽象工厂接口里,可以定义多个方法(即具体类可以创建多个产品实例)
其实大多数情况下,工厂方法已经能满足需求,抽象工厂可用可不用
控制反转
一个类中用到其他类的实例时,我们不在该类中创建实例,而是在类外创建实例后把实例作为参数传入类中
高阶编程中我们希望高层调用和低层模块之间尽量做到低耦合,避免高层因参数变化就需要修改低层代码。
以上面代码为例,工厂类虽然规范了类的调用和实现,但是当增加新的数据库类时,我们还需要在工厂增加新的判断。
但如果只是简单的实现控制反转,那我们又回了最初没有使用工厂模式时的状态,需要手动的判断和创建需要用到的对象。所以针对控制反转的思想,诞生了依赖注入设计模式
依赖注入
当需要创建大量的类的实例的时候,我们为了方便管理,把类实例化的过程分离出来,并存把实例化对象存放在一个数组或对象中(即容器),然后我们在类中直接从容器中获取我们需要的实例即可。这就是依赖注入设计模式的基本思想
初级实现
下面我们通过实现一个缓存模块,来展示依赖注入和容器的基本使用。
首先,建立一个容器类Di.php
<?php
class Di
{
//存储依赖实例的数组
protected $_definitions= [];
//把对象存入实例数组,并指定名称
public function set($name, $definition)
{
$this->_definitions[$name] = $definition;
}
//根据名称,获取一个实例
public function get($name)
{
if (isset($this->_definitions[$name])) {
$definition = $this->_definitions[$name];
} else {
throw new Exception("class not exist");
}
if (is_object($definition) || is_callable($definition)) {
$instance = call_user_func($definition);
}else{
throw new Exception("class not obj");
}
}
}
缓存类型有file、Db、Redis,我们建立这三种类型对应的操作类
//创建接口
interface BackendInterface{
public function find($key, $lifetime);
public function save($key, $value, $lifetime);
public function delete($key);
}
//实现接口
class redis implements BackendInterface
{
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
class db implements BackendInterface
{
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
class file implements BackendInterface
{
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
建立Cache类,即被注入类
class cache
{
//实例容器
protected $_di;
//当前类实例化时传入的参数
protected $_options;
//当前缓存用到的媒介对象。即上面创建的db,redis,file中的一种
protected $_connect;
public function __construct($options)
{
$this->_options = $options;
}
//把外部容器传入对象中
public function setDI($di)
{
$this->_di = $di;
$options = $this->_options;
//根据实例化时传入的参数,获取当前缓存要使用的媒介对象名称
if (isset($options['connect'])) {
$service = $options['connect'];
} else {
//没传,则默认使用redis
$service = 'redis';
}
//根据媒介对象名称,从容器中找出对应的实例
$this->_connect = $this->_di->get($service);
}
public function get($key, $lifetime)
{
$connect = $this->_connect;
return $connect->find($key, $lifetime);
}
public function save($key, $value, $lifetime)
{
$connect = $this->_connect;
return $connect->save($key, $lifetime);
}
public function delete($key)
{
$connect = $this->_connect;
$connect->delete($key, $lifetime);
}
}
我们看到cache的实现,需要依赖BackendInterface接口的实例。所以我们第一步首先把BackendInterface的实例注入到容器中,如下
$di = new Di();
// 往Di容器中注入redis实例
$di->set('redis', function() {
return new redis([
'host' => '127.0.0.1',
'port' => 6379
]);
});
$di->set('file', function() {
return new file([
'paht' => 'cache/file_cach.log'
]);
});
$di->set('db', function() {
return new db([
'host' => '127.0.0.1',
'port' => 3306
]);
});
把可能用到的实例注入到容器中后,把容器传给cache对象,cache就可从容器中拿到自己需要的实例来操作内容,代码如下
//实例化cache对象,并使用参数控制想使用的缓存类型
$cache = new cache(['connent'=>'file']);
//把容器传入cache对象
$cache->setDI($di);
// 调用cache方法实现缓存
$cache->save();
初级实现的问题
- 以上过程实现了简单的依赖注入和容器,但是我们发现,上面的依赖关系只有一层。如果将Db类进行扩展,可以选择使用mysql或orcal数据库。
那么此时依赖关系就变成了三层,在把db实例注入容器前,需要先把mysql和orcal的实例注入到容器中,否则在db中调用容器中的mysql或orcal实例时,将无法找到。 - 当依赖层级较多的时候,一个个的注入不仅不方便,一旦顺序错误也会造成错误。这个时候我们就需要使用php的ReflectionClass反射机制,构建一个自动注入且不需要关心注入顺序的Di容器。
中级实现
初级实现中,我们需要先把依赖的实例注入到Di容器中,在被注入的类中需要用到时从Di容器中取。
中级实现,则是自动注入。在被注入函数实例化或者方法被调用时,根据构造函数或方法中指定的参数类型,把对象类型的参数实例化后再传入被注入类中。而要获取类的各种信息,就需要用到反射类 ReflectionClass
改造后Di代码如下
class Di {
//保存实例的数组
protected $_definitions=[];
// 获得类的对象实例
public static function getInstance($className) {
//根据类名,从实例数组中查找实例。存在则返回
if(isset($this->_definitions[$className])) return $this->_definitions[$className];
//实例不存在,则获取该类下构造方法中的参数的实例
$paramArr = self::getMethodParams($className);
//使用反射,生成实例,并插入到实例数组中
$this->_definition[$className] = (new ReflectionClass($className))->newInstanceArgs($paramArr);
return $this->_definition[$className];
}
//直接调用类的方法
public static function make($className, $methodName, $params = []) {
$instance = self::getInstance($className);
// 获取方法所需要依赖注入的参数
$paramArr = self::getMethodParams($className, $methodName);
//
return $instance->$methodName(array_merge($paramArr, $params));
}
// 获得类的方法参数,把对象类参数实例化
protected static function getMethodParams($className, $methodsName = '__construct') {
// 通过反射获得该类的信息
$class = new ReflectionClass($className);
$paramArr = []; // 记录参数,和参数类型
// 判断函数方法名是否存在
if ($class->hasMethod($methodsName)) {
$construct = $class->getMethod($methodsName);
// 判断函数方法是否有参数
$params = $construct->getParameters();
if (count($params) > 0) {
// 遍历参数,并判断参数类型
foreach ($params as $key => $param) {
//如果参数名是一个存在的类,说明需要实例化该类作为依赖
if (class_exists($param->name)) {
//递归获取依赖的对象是否依赖其他对象
$args = self::getMethodParams($param->name);
//返回依赖对象的实例作为参数
$paramArr[] = (new ReflectionClass($param->name))->newInstanceArgs($args);
}
}
}
}
return $paramArr;
}
}
cache和db代码改造后如下
class cache
{
protected $_connect;
public function __construct(db $db)
{
$this->_connect= $db;
}
......
}
class db implements BackendInterface
{
public $_connect;
public __construct(mysql $mysql){
$this->_connect = $mysql->connect();
}
public function find($key, $lifetime) {
$connect = $this->_connect();
return $connect->find();
}
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
}
//调用
$cache = Di::getInstance('cache');
改造后我们无需注册依赖,就可以直接调用实例化cache并调用方法。容器会根据依赖的类型名称,自动查找并实例化类。
中级实现的问题
- 自动实例化时,必须能够保证能在当前路径够找到这些类。这就需要提前引入需要用的的类的命名空间地址,这导致项目复杂时,需要在容器类中引入所有可能用到的类,而且后期还需要不断的增加,这显然是不优雅的
- 当我们需要调整cache的存储方式为redis时,发现不能通过传入参数调整
高级实现
针对中级实现的问题,我们需要进行两点优化
- 可动态引入需要使用类的命名空间
- 可传入参数,控制实例化过程
为解决以上两点,还是要引入注册机制。但此时是的注册不像初级实现那样注册对象。这里的注册仅指定依赖关系,而不是实例化依赖对象。当Di解析依赖时,可根据注册的依赖关系获取依赖对象。且此时注册也不需要分先后顺序。
调用代码
$di = new Di();
//注册依赖关系时,指定了类的路径,容器中就不需引入类的命名空间
//注册不分先后顺序,因为此时并没有实例化,只是注册了依赖关系
$di->set('cache',['class'=>'\\cache'])
$di->set('BackendInterface ',['class'=>'\\app\\db']);
//注册依赖关系时,也指定了类实例化时的参数,用以控制实例化过程
//若我们要用redis方式存储缓存数据,可以修改后面的配置信息
//如:$di->set('BackendInterface ',['class'=>'\\app\\redis','host'=>'','name'=>'']);
$di->set('sql',['class'=>'\\sql\\mysql','host'=>'127.0.0.1','name'=>'test']);
//调用时自动解析依赖并实例化
$cache = $di->get('cache');
容器代码
class Di {
protected $_dependencies=[];//存储依赖对象实例
protected $_definitions=[];//存储依赖注册信息
// 注册依赖关系
public function set($className,$param) {
//查看依赖是否注册,已注册直接返回
if(isset($this->_definitions[$className])) return $this->_definitions[$className];
//注册依赖映射
//依赖名称不是类名,可自定义。类名通过参数中的class指定
$this->_definitions[$className] = $param;
}
// 获取依赖
public function get($className) {
//实例数组中存在,直接返回
if(isset($this->_dependencies($className))) return $this->_dependencies($className));
//实例不存在,使用反射生成所需依赖的实例
//获取实例化依赖类需要的参数
$paramArr = $this->getMethodParams($className);
//实例化依赖并存入依赖数组
$this->_dependencies[$className] = (new ReflectionClass($className))->newInstanceArgs($paramArr);
return $this->_dependencies[$className];
}
// 获得类的方法参数,把对象类参数实例化
protected function getMethodParams($className) {
// 通过反射获得该类的信息
$class = new ReflectionClass($this->_definitions[$className]['class']);
// 记录构造方法参数
$paramArr = [];
// 判断构造函数方法名是否存在
$methodsName = '__construct'
if ($class->hasMethod($methodsName)) {
$construct = $class->getMethod($methodsName);
// 判断函数方法是否有参数
$params = $construct->getParameters();
if (count($params) > 0) {
foreach ($params as $key => $param) {
// 判断参数类型,如果参数名称在注册信息数组中,说明是对象,递归实例化参数
if (in_array($param->name,array_keys($this->_definitions))) {
//递归获取依赖的对象是否依赖其他对象
$args = self::getMethodParams($param->name);
//返回依赖对象的实例作为参数
$paramArr[] = (new ReflectionClass($this->_definitions[$param->name]['class']))->newInstanceArgs($args);
}else{
//如果参数是普通值,从依赖注册数组中查看是否有对应的值
if(isset($this->_definitions[$className][$param->name])){
$paramArr[] = $this->_definitions[$className][$param->name];
}
}
}
}
}
return $paramArr;
}
}
class cache
{
protected $_connect;
//类的依赖信息放在构造函数中,便于在实例化类时解析相关依赖。同时也符合了迪米特法则
//解析依赖时根据指定的实例路径生成映射,我们从上面Di的get代码中看到,获取依赖对象的映射时
//并不是直接使用的指定路径,而是通过指定的路径从_definitions中获取真实的依赖类信息,这样我们需要用到不同模块时,只要修改配置中的class等信息即可
public function __construct(BackendInterface $cache)
{
$this->_connect= $cache;
}
public function get($key, $lifetime)
{
$connect = $this->_connect;
return $connect->find($key, $lifetime);
}
public function save($key, $value, $lifetime)
{
}
public function delete($key)
{
}
}
class db implements BackendInterface
{
public $connect;
public __construct(sql $db,$host='localhost',$port='3306'){
$this->connect = $db;
}
public function find($key, $lifetime) {
return $this->connect->find();
}
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
}