浅谈依赖注入(DI)和控制反转(IoC)
依赖注入(DI)和控制反转(IoC)乍一看是很高端的东西,其实就是近年来兴起的一种思想,而又不仅仅是编程思想。主要是协调各组件间相互的依赖关系,同时大大提高了组件的可移植性,组件的重用机会也变得更多,接下来我们来了解一下这两个东西究竟是什么东西。
一、依赖注入(DI)
简单地讲,只要不是由内部生产(比如初始化、构造函数 __construct 中通过工厂方法、自行手动 new 的),而是由外部以参数或其他形式注入的,都属于依赖注入(DI)
举个栗子,依赖就是实现一件事情的前提,例如你想生孩子,嗯那好,你得首先有个老婆,现在没车没房没存款哪这么容易讨得到老婆,呸呸,扯远了。
class Baby
{
protected $wife;
public function __construct(Wife $wife)
{
$this->wife = $wife;
}
}
$wife = new Wife();
$baby = new Baby($wife);
如代码所示,要生个孩子,你得有个老婆,老婆就是生孩子的依赖,并且需要在构造是将依赖注入才能实例化。
有人说那要生孩子只需要有个老婆而已,这样也不难啊,我只能说兄弟你太年轻了,先在如果生孩子你岳母还要你有房子、车子、准生证....一大堆前提,甚至科学技术发达以后生孩子连老婆都不需要了呢,这样我们就不得不重写Baby类了,这样显然耦合性太高了。这时候我们可以想到用工厂模式来组模,即将生孩子的条件在一个工厂类中完成,这样Baby类就从原来对各种类的依赖变成了对工厂类的依赖,如果生孩子的依赖变了我们不需要修改Baby类而只需要修改这个工厂类。
Baby类如下:
class Baby
{
protected $condition;
public function __construct(array $modules)
{
// 初始化工厂
$factory = new BirthbabyModuleFactory();
// 通过工厂提供的方法制造需要的模块
foreach ($modules as $key=>$value) {
$this->condition[] = $factory->makeModule($key, $value);
}
}
}
工厂类如下:
class BirthbabyModuleFactory
{
public function makeModule($moduleName, $options)
{
switch ($moduleName) {
case 'Wife':
return new Wife($options[0], $options[1]);
case 'Car':
return new Car($options[0]);
case 'Build':
return new Build($options[0], $options[1], $options[2]);
}
}
}
然而,工厂类还是有一定的不足,那就是,接口未知,即没有一个很好的契约,这时我们需要提出一种契约,这样无论是谁创造出的模组,都符合这样的接口,自然就可被正常使用,于是我们想到了接口( interface )。
因为一个 对象(object) 本身是由他的模板或者原型 —— 类 (class) ,经过实例化后产生的一个具体事物,而有时候,实现统一种方法且不同功能(或特性)的时候,会存在很多的类(class),这时候就需要有一个契约,让大家编写出可以被随时替换却不会产生影响的接口。
interface BirthbabyModuleInterface
{
public function activate(array $target);
}
//所有implements这个interface的类必须遵循interface的方法
class Wife implements BirthbabyModuleInterface
{
public function activate(array $target)
{
//
}
}
class Car implements BirthbabyModuleInterface
{
public function activate(array $target)
{
//
}
}
然而不管怎样,在初始化Baby对象的时候总是需要手动注入依赖,在人类社会高度发达的今天居然还要手动生孩子?不是说好的坐上来自己动的吗。于是更高级的工厂应运而生——IoC 容器。
二、控制反转(IoC)
控制反转,顾名思义,就是把控制权反转过来,举个栗子,以前生孩子需要有老婆、房子、准生证。。。,控制权在老婆、房地产商、街道办事处的手上,你要生孩子得征得他们的同意,现在呢你有一个好妈妈,她也希望早点抱孙子,于是他把你生孩子的一切条件都准备好了,老婆哪来的?越南买的。这时候你就会发现生孩子这件事你变成了话事人,你想啥时候生啥时候生,想生几个生几个,这个好妈妈把老婆、车子、准生证都绑定在一个容器中让你随时能拿,这个容器就是IoC 容器。简单地说,一个类需要绑定、注册至容器中,才能被“制造,然后还需要有一个专门提供这个容器中的服务的服务提供者,下面是一个简单地容器类:
class Container
{
protected $binds;
protected $instances;
//绑定
public function bind($abstract, $concrete)
{
if ($concrete instanceof Closure) {
$this->binds[$abstract] = $concrete;
} else {
$this->instances[$abstract] = $concrete;
}
}
//提供
public function make($abstract, $parameters = [])
{
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
array_unshift($parameters, $this);
return call_user_func_array($this->binds[$abstract], $parameters);
}
}
通过最初的 绑定(bind) 操作,我们向 超级工厂 注册了一些生产脚本,这些生产脚本在生产指令下达之时便会执行。发现没有?我们彻底的解除了 孩子 与 生孩子组模 的依赖关系,更重要的是,容器类也丝毫没有和他们产生任何依赖!我们通过注册、绑定的方式向容器中添加一段可以被执行的回调(可以是匿名函数、非匿名函数、类的方法)作为生产一个类的实例的脚本 ,只有在真正的 生产(make) 操作被调用执行时,才会触发。
$container = new Container;
// 向该 超级工厂添加孩子的生产脚本
$container->bind('baby', function($container, $moduleName) {
return new Baby($container->make($moduleName));
});
// 向该 超级工厂添加超能力模组的生产脚本
$container->bind('xpower', function($container) {
return new wife;
});
$container->bind('ultrabomb', function($container) {
return new house;
});
// 开始启动生产
$superman_1 = $container->make('baby', 'wfie');
$superman_2 = $container->make('baby', 'house');
$superman_3 = $container->make('baby', 'car');
总的来说,IoC容器就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦
最后再引出一个经典的栗子,那就是关于数据库连接的。假如你有一个类需要用到数据库,这时候你可以这样写:
class A
{
public function __construct()
{
$connection = new Connection(array(
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo"
));
}
}
$some = new A();
有的老铁说没毛病,然而显而易见的是你没次初始化这个类的对象你都要重新连接一次数据库,更重要的是但你想把数据库从oracle换成Mysql的时候你又得去修改这个类,这样耦合度就高了,解决办法就是把创建一个数据库连接类并且注册到服务容器中去,然后初始化,这样这个数据库实例就存在容器中了,实际上这个数据库连接类是一个单例,不会一直初始化,这样其实就是一个解耦的过程。
上图,途中箭头为依赖关系
image.png image.png
本来各个类之间错综复杂的相互依赖变成了各个类对IoC容器的依赖,类A、B、C、D、间不存在耦合,类与类之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
以上都是本人自己的见解,如有不对,那你又能怎样。