控制反转(IOC)、依赖注入(DI)、容器(Container)
(简单读一下就行)
当调用者需要被调用者的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例,但在这里,创建被调用者的工作不再由调用者来完成,而是将被调用者的创建移到调用者的外部,从而反转被调用者的创建,消除了调用者对被调用者创建的控制,因此称为控制反转。
要实现控制反转,通常的解决方案是将创建被调用者实例的工作交由IoC容器来完成,然后在调用者中注入被调用者(通过构造器/方法注入实现),这样我们就实现了调用者与被调用者的解耦,该过程被称为依赖注入。依赖注入是控制反转的一种实现方式。常见注入方式有三种:setter、constructor injection、property injection。
管理对象的生成、资源取得、销毁等生命周期,建立对象与对象之间的依赖关系,可以延时加载对象。比较著名有PHP-DI、Pimple。
这里我们以电脑USB
接口为例,USB
接口可以插入鼠标
、键盘
、U盘
等等一系列的设备。这个时候我们定义一个USB
类,假设执行的是Mouse
类鼠标。
/**
* 定义鼠标类
*/
class Mouse
{
/**
* 鼠标类执行方法
*/
public function run()
{
echo '鼠标正常运行';
}
}
/**
* USB 类
*/
class USB
{
/**
* 接入设备
*/
private $device;
public function __construct()
{
$this->device = new Mouse();
}
/**
* USB 设备运行,这里我们运行的是鼠标
*/
public function work()
{
$this->device->run();
}
}
(new USB())->work();
// 这里输出 '鼠标正常运行'
这个时候USB
类中的$device
是内部实例化的并且写死了Mouse
, 耦合度太高。这里我们假设有一个Keyboard
类,USB
中切换到Keyboard
不使用Mouse
时,我们就需要在USB
的__construct
方法中将new Mouse()
改成new Keyboard()
。这样没问题,但是如果再改成别的设备,这个类还待需要内部改。这个时候我们可以将设备实例化后当成参数传入到USB
类时,这样就不需要每次修改内部的$device
这就是控制反转。
/**
* 定义鼠标类
*/
class Mouse
{
/**
* 鼠标类执行方法
*/
public function run()
{
echo '鼠标正常运行';
}
}
/**
* 定义键盘类
*/
class Keyboard
{
/**
* 键盘类执行方法
*/
public function run()
{
echo '键盘正常运行';
}
}
/**
* USB 类
*/
class USB
{
/**
* 接入设备
*/
private $device;
public function __construct($device)
{
$this->device = $device;
}
/**
* USB 设备运行,这里我们运行的是鼠标
*/
public function work()
{
$this->device->run();
}
}
(new USB(new Mouse()))->work();
// 这里输出 '鼠标正常运行'
(new USB(new Keyboard()))->work();
// 这里输出 '键盘正常运行'
- 这里我们
USB
的控制权交给了外部的new Mouse()
new Keyboard()
这种思想就是控制反转,而在USB
类__construct($device)
传入的$device
就是依赖注入,- 注入的方式有三种,分别是:基于构造函数、基于 setter 方法、基于接口。其中我们使用的就是基于构造函数。
一般电脑的USB
设备都有自己的协议,鼠标、键盘、U盘如果需要正常使用就必须要实现USB
协议,这里我们就要使用接口约束
,看代码
/**
* 定义设备接口
*/
interface Device
{
public function run();
}
/**
* 定义鼠标类
*/
class Mouse implements Device
{
/**
* 鼠标类执行方法
*/
public function run()
{
echo '鼠标正常运行';
}
}
/**
* 定义键盘类
*/
class Keyboard implements Device
{
/**
* 键盘类执行方法
*/
public function run()
{
echo '键盘正常运行';
}
}
/**
* USB 类
*/
class USB
{
/**
* 接入设备
*/
private $device;
public function __construct(Device $device)
{
$this->device = $device;
}
/**
* USB 设备运行,这里我们运行的是鼠标
*/
public function work()
{
$this->device->run();
}
}
(new USB(new Mouse()))->work();
// 这里输出 '鼠标正常运行'
(new USB(new Keyboard()))->work();
// 这里输出 '键盘正常运行'
- 自动管理依赖关系,避免手工管理存在缺陷。
- 需要使用依赖时自动注入所需依赖。
- 管理对象生命周期。
无论是
Laravel
还是Thinkphp 5
以后的版本都使用了Container
, 譬如我们经常写类似如下的代码
class IndexController
{
public function test(Request $request)
{
dump($request);
}
}
我们会发现Request
类被自动注入切实例化能够使用了,这就是容器(Container)
帮我们把依赖自动[注入]到类中。核心的原理其实就是我们通过
反射
来将类中的方法实例化,主要还是用到了反射,这里我就简单写一个[容器]的类
/**
* 容器类
*/
class Container
{
/**
* 容器对象实例
* @var Container
*/
protected static $instance;
/**
* 容器中实例化对象(laravel中使用singleton)
* @var array
*/
protected $instances = [];
/**
* 容器中的绑定标识 (laravel 中使用 Bind)
* @var array
*/
protected $binds = [];
/**
* 初始化容器 (单例)
*/
public static function getInstance()
{
// 如果 Container 没有初始化
if(is_null(self::$instance)) {
// 这里使用 new static 而不是new self
// 当使用 new static使用,当有类库继承 Container 时,实例化的是继承类
// 使用 new self 实例化的是 Container 本身,所以这里我们使用 new static
self::$instance = new static;
}
// ... 这里不用讲解了吧
return self::$instance;
}
/**
* 绑定类、闭包,实例等到容器中
*/
public function bind($abstract, $concrete = null)
{
// 当绑定为null时候 $container->bind(Test:class);
// $abstract = $concrete
if(is_null($concrete)) {
$abstract = $concrete;
}
// 删除单例绑定
if(isset($this->instances[$abstract])) {
unset($this->instances[$abstract]);
}
// 如果不是闭包
if(! $concrete instanceof Closure) {
// 这里我们创建闭包,让返回都统一是闭包
$concrete = function($container, $params = []) use ($abstract, $concrete) {
// 当我们使用 $container->bind(Test:class, Test:class) 时
// 我们就通过闭包的方式返回实例化的具体实例
if($abstract == $concrete) {
return $container->build($concrete, $params);
}
// 当不相等的时候 $container->bind(TestInterface::class, Test::class)
// 我们就通过容器来解析指定的类型
return $container->resolve($concrete, $params);
};
}elseif(is_object($concrete) && !$concrete instanceof Closure) {
// 如果传入的是实例化的类
$this->instances[$abstract] = $concrete;
}
// 绑定
$this->binds[$abstract] = $concrete;
// 返回容器本身
return $this;
}
/**
* 通过反射实例化类
*/
public function build($concrete, $params)
{
// 这里我们通过反射的原理来实例化构建
$reflect = new ReflectionClass($concrete);
// 先判断是否可以实例化
if(!$reflect->isInstantiable()) {
// 这里先直接返回false
// 正常应该 throw \Exception('错误问题说明等');
return false;
}
// 获取类里边的 __construct() 方法
$constructor = $reflect->getConstructor();
// 如果不存在 __construct 方法, 直接返回实例
if( !$constructor ) {
return new $concrete;
}
// 否则获取 constructor 方法里的参数
$parameters = $constructor->getParameters();
// 解析参数
$args = $this->resolveParameters($parameters, $params);
// 返回实例
return $reflect->newInstanceArgs($args);
}
/**
* 通过反射解析参数 注入参数
*/
public function resolveParameters($parameters, array $params = [])
{
$args = [];
foreach($parameters as $parameter) {
// 注入参数的类型
$reflectType = $parameter->getType();
// 参数名称
$paramName = $parameter->getName();
// 动态参数
if($parameter->isVariadic()) {
return array_merge($args, array_values($params));
} elseif ($reflectType && $reflectType instanceof ReflectionNamedType && !$reflectType->isBuiltin()){
// 如果注入参数类型是class 那就实例化注入
$args[] = $this->make($reflectType->getName());
} elseif ( array_key_exists($paramName, $params)) {
// 查找数组中是否有指定的参数名称
$args[] = $params[$paramName];
// 删除
unset($params[$paramName]);
} elseif (!empty($params)) {
// 如果是数组且没有指定 key 时候从头部按照顺序当做参数使用
$args[] = array_shift($params);
} elseif ($parameter->isDefaultValueAvailable()) {
$args[] = $parameter->getDefaultValue();
}
}
return $args;
}
/**
* 在容器中解析指定的类型
*/
public function resolve($abstract, $parameters = [])
{
if(isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
// 如果存在绑定并且还是闭包的情况下,我们要进行解析
if(isset($this->binds[$abstract]) && $this->binds[$abstract] instanceof Closure) {
$function = $this->binds[$abstract];
$object = $function($this, $parameters);
}else{
// 不存在的情况下我们就通过反射来实例化这个类
$object = $this->build($abstract, $parameters);
}
// 返回实例
return $object;
}
/**
* 在容器中解析类的实例
*/
public function make($abstract, $params = [])
{
return $this->resolve($abstract, $params);
}
/**
* 解析容器的实例,并执行对应的方法
*/
public function makeWithMethod($abstract, $method = '__construct', $params = [])
{
$object = $this->resolve($abstract, $params);
if($method == '__construct'){
return $object;
}
$reflect = new ReflectionMethod($object, $method);
// 否则获取 constructor 方法里的参数
$parameters = $reflect->getParameters();
// 解析参数
$args = $this->resolveParameters($parameters, $params);
// 调用类方法
return $reflect->invokeArgs($object, $args);
}
/**
* 单例绑定模式,在后续调用中返回相同的实例
*/
public function singleton($abstract, $instance)
{
// 如果是闭包,我们将 Container 本身传递给闭包 这样就可以在闭包中使用引用
if($instance instanceof Closure) {
$this->instances[$abstract] = $instance($this);
}else{
$this->instances[$abstract] = $instance;
}
}
}
然后我们开始模拟测试
// 获取容器单例
$container = Container::getInstance();
// 定义一个接口
interface USB {
public function work();
}
// 定义Mouse
class Mouse implements USB
{
public function work()
{
return 'Mouse is Work';
}
}
// 这里我们绑定 USB 接口为一个闭包回调,然后返回 USB 类接口
$container->bind(USB::class, function($container, $params){
return 'USB类接口';
});
//然后解析类 输出 'USB类接口'
echo $container->make(USB::class);
// 我们继续绑定
$container->bind(USB::class, Mouse::class);
// 然后解析类 输出 'USB类接口'
echo $container->make(USB::class)->work();
// 我们经常遇到是如 Laravel 执行控制器中的方法,这个如何操作
/**
* 模拟定义 Request
*/
class Request
{
public function all()
{
var_dump($_REQUEST);
}
}
/**
* 模拟登陆类
*/
class LoginController
{
public function Login(Request $request, $city)
{
$request->all();
var_dump('城市:'. $city);
}
}
// 模拟容器执行, 携带参数
$container->makeWithMethod(LoginController::class, 'Login', ['city' => '北京']);
// 当我们浏览器执行 http://localhost/c.php?a=1 的时候
// 就会输出如下
/* array(1) {
["a"]=>
string(1) "1"
}
string(6) "北京"
*/
通过上边我们就基本能够清楚控制反转(IOC)
、依赖注入(DI)
、容器(Container)
,如果哪里不清楚或者需要细讲的地方,请留言