一次搞懂几种常用的设计模式


设计模式虽多,但实际开发中,我们常用的无非是简单的几种,即:工厂模式、依赖倒置、控制反转、依赖注入

简单工厂模式

简单工厂模式是编程中用到的最初级的设计模式,主要作用是分离代码中重复的建立对象的过程,实现代码的复用。

比如,当我们需要项目中根据需求不同,要链接不同类型的数据库,以下是一个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) { }
    }
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值