IoC 容器
梳理 IOC 容器 与 依赖注入
依赖注入:
依赖注入的理解,举个例子,假设我是一个钢铁侠,我需要满足打坏人的需求,对于我来说我依赖于战衣(毕竟我没有战衣和普通人一样).
所谓“依赖”,就是 “我若依赖你,我就不能离开你”。
以简单的例子来说这个依赖不算什么,如果这个量级很恐怖,我需要满足打全世界的坏人,各种能力的坏人出现,在很大的依赖体系下,依赖是多么恐怖.当然既然我提到了这个问题,肯定有解决方案.
##可怕的依赖
接上个例子, 我是钢铁侠,我需要按照不同的敌人制作不同的战衣:
//战衣
class Armor
{
//飞行
protected $flight;
//GPS
protected $gps;
//等离子发射器
protected $plasma;
public function __construct($flight, $gps, $plasma){}
}
假设我制造的战衣的能力:
斥力发射器
GPS全球定位系统
等离子发射器
//飞行 class Flight { //飞行高度 protected $height; //飞行速度 protected $speed; public function __construct($height, $speed){} } //GPS class Gps { //精度 protected $accuracy; public function __construct($accuracy){} } //等离子发射器 class Plasma { //范围 protected $range; public function __construct($range){} }
现在我需要在初始化的时候根据敌人能力得到相应的战衣.
//钢铁侠 class IronMan { //斥力发射器GPS全球定位系统等离子发射器 //能力 protected $power; function __construct() { $this->power = new Flight('12', '20'); } }
可以看出来当能力很多的时候,我要一一实例,将会是一个噩梦,现在我的战衣的属性是斥力发射器、GPS全球定位系统、等离子发射器。如果需求变更,需要安全性、防御能力、还要破坏力等一系列需求。我还要一一实例,等我实例出来了,敌人会等我吗? 我拒绝!!!
在如此效率低下的情况下,我们不能固化战衣初始化的能力,而转由外部负责,由外部创造战衣属性的模组、飞行或者等离子发射器等(我们后面统一称为 “模组”),植入车的某一个接口,这个接口是一个既定的,只要这个 “模组” 满足这个接口的装置都可以被钢铁侠所利用,可以提升、增加钢铁侠的某一种属性。这种由外部负责其依赖需求的行为,我们可以称其为 “控制反转(IoC)”。控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
工厂模式,依赖转移
当然,实现控制反转的方法有几种。在这之前,不如我们先了解一些好玩的东西。
我们可以想到,组件、工具,是一种可被生产的玩意儿,生产的地方当然是 “工厂(Factory)”,于是有人就提出了这样一种模式: 工厂模式。
工厂模式,顾名思义,就是一个类所依赖的外部事物的实例,都可以被一个或多个 “工厂” 创建的这样一种开发模式,就是 “工厂模式”。
我们为了给超人制造超能力模组,我们创建了一个工厂,它可以制造各种各样的模组,且仅需要通过一个方法:
//战衣工厂
class ArmorModuleFactory
{
public function makeModule($moduleName, $options)
{
switch ($moduleName) {
case 'Flight': return new Flight($options[0], $options[1]);
case 'Gps': return new Gps($options[0]);
case 'Plasma': return new Plasma($options[0]);
}
return false;
}
}
这时候,战衣创建之初就可以使用这个工厂!
//钢铁侠
class IronMan
{
//能力
protected $power;
function __construct()
{
$factory = new ArmorModuleFactory();
$this->power = $factory->makeModule('Flight', ['10', '20']);
}
}
可以看得出,我们不再需要和之前一样去初始化许多第三方类,只需初始化一个工厂类,即可满足需求。但这样似乎和以前区别不大,只是没有那么多 new
关键字。其实我们稍微改造一下这个类,你就明白,工厂类的真正意义和价值了。
//钢铁侠
class IronMan
{
//能力
protected $power;
function __construct(array $modules)
{
$factory = new ArmorModuleFactory();
foreach ($modules as $moduleName => $moduleOptions){
$this->power = $factory->makeModule($moduleName,$moduleOptions);
}
}
}
//创建钢铁侠
$customer = new IronMan([
'Flight' =>['10', '20'],
'Gps' =>['8']
]);
现在修改的结果令人满意。现在,“战衣” 的创建不再依赖任何一个 “能力” 的类,我们如若修改了或者增加了新的能力属性,只需要针对修改 ArmorModuleFactory
即可。扩充超能力的同时不再需要重新编辑钢铁侠的类文件,使得我们变得很轻松。但是,这才刚刚开始。
再进一步!IoC 容器的重要组成 —— 依赖注入!
由钢铁侠依赖战衣的属性变成了钢铁侠依赖”战衣属性工厂”,制作起来更加的得心应手。但是依赖还是并未解除, 如果工厂出现了问题,问题将会变的非常复杂。
其实大多数情况下做到这一步基本已足够了。
工厂模式的缺点就是:接口未知(即没有一个很好的契约模型,关于这个我马上会有解释)、产生对象类型单一。总之就是,还是不够灵活。虽然如此,工厂模式依旧十分优秀,并且适用于绝大多数情况。不过我们为了讲解后面的 依赖注入 ,这里就先夸大一下工厂模式的缺陷咯。
我们都知道钢铁侠依赖战衣的模组,我们要求有一个统一接口,这样才能和钢铁侠身上的注入接口对接,最终起到提升战衣属性的效果。
现在出现了更加强大又可怕的敌人,以前的能力已经完全跟不上敌人出现的速度了。这时候似乎工厂的生产能力显得有些不足 —— 由于工厂模式下,所有的模组都已经在工厂类中安排好了,如果有新的、高级的模组加入,我们必须修改工厂类(好比增加新的生产线)。
//战衣工厂
class ArmorModuleFactory
{
public function makeModule($moduleName, $options)
{
switch ($moduleName) {
case 'Flight': return new Flight($options[0], $options[1]);
case 'Gps': return new Gps($options[0]);
case 'Plasma': return new Plasma($options[0]);
// case 'more': .......
// case 'and more': .......
// case 'and more': .......
// case 'oh no! its too many!': .......
}
return false;
}
}
又出现了噩梦!!!
其实灵感就差一步!你可能会想到更为灵活的办法!对,下一步就是我们今天的主要配角 —— DI (依赖注入)
由于需求不断增大,我们需要集合整个世界的高智商人才,一起解决问题,不应该仅仅只有几个工厂垄断负责。不过高智商人才们都非常自负,认为自己的想法是对的,创造出的超能力模组没有统一的接口,自然而然无法被正常使用。这时我们需要提出一种契约,这样无论是谁创造出的模组,都符合这样的接口,自然就可被正常使用。
interface ArmorModuleInterface
{
/**
* 战衣能力激活方法
*
* 任何一个能力都得有该方法,并拥有一个参数
*@param array $target 针对目标,可以是一个或多个,自己或他人
*/
public function activate(array $target);
}
定义了一个契约,所有创建的模组都必须遵守规范,才能生产。
其实,这就是 php 中 接口( interface ) 的用处和意义!很多人觉得,为什么 php 需要接口这种东西?难道不是 java 、 C# 之类的语言才有的吗?这么说,只要是一个正常的面向对象编程语言(虽然 php 可以面向过程),都应该具备这一特性。因为一个 对象(object) 本身是由他的模板或者原型 —— 类 (class) ,经过实例化后产生的一个具体事物,而有时候,实现统一种方法且不同功能(或特性)的时候,会存在很多的类(class),这时候就需要有一个契约,让大家编写出可以被随时替换却不会产生影响的接口。这种由编程语言本身提出的硬性规范,会增加更多优秀的特性。
虽然有些绕,但通过我们接下来的实例,大家会慢慢领会接口带来的好处
这时候,那些提出更好的超能力模组的高智商人才,遵循这个接口,创建了下述(模组)类:
/**
* X-超能量
*/
class XPower implements ArmorModuleInterface
{
public function activate(array $target)
{
// 这只是个例子。。具体自行脑补
}
}
/**
* 终极炸弹 (就这么俗)
*/
class UltraBomb implements ArmorModuleInterface
{
public function activate(array $target)
{
// 这只是个例子。。具体自行脑补
}
}
同时,为了防止有些 “砖家” 自作聪明,或者一些叛徒恶意捣蛋,不遵守契约胡乱制造模组,影响超人,我们对超人初始化的方法进行改造:
//钢铁侠
class IronMan
{
//能力
protected $module;
function __construct(ArmorModuleInterface $module)
{
$this->module = $module;
}
}
改造完毕!现在,当我们初始化 “钢铁侠” 类的时候,提供的模组实例必须是一个 ArmorModuleInterface 接口的实现。否则就会提示错误。
正是由于钢铁侠的创造变得容易,一个钢铁侠也就不需要太多的战衣,我们可以创造多个钢铁侠,并分别注入需要的战衣模组即可。这样的话,虽然一个钢铁侠只有一个战衣,但钢铁侠更容易变多,我们也不怕敌人啦!
现在有人疑惑了,你要讲的 依赖注入 呢?
其实,上面讲的内容,正是依赖注入。
什么叫做 依赖注入?
本文从开头到现在提到的一系列依赖,只要不是由内部生产(比如初始化、构造函数 __construct
中通过工厂方法、自行手动 new
的),而是由外部以参数或其他形式注入的,都属于 依赖注入(DI) 。是不是豁然开朗?事实上,就是这么简单。下面就是一个典型的依赖注入:
// 战衣能力模组
$armorModule = new XPower;
// 初始化一个钢铁侠,并注入一个能力模组依赖
$IronMan = new IronMan($armorModule);
关于依赖注入这个本文的主要配角,也就这么多需要讲的。理解了依赖注入,我们就可以继续深入问题。慢慢走近今天的主角……
更为先进的工厂 —— IoC 容器!
刚刚列了两段代码
// 战衣能力模组
$armorModule = new XPower;
// 初始化一个钢铁侠,并注入一个能力模组依赖
$IronMan = new IronMan($armorModule);
手动创建一个能力组,并且注入这就导致每次都要手动,懒。
现代社会,应该是高效率的生产,干净的车间,完美的自动化装配。
一群敌人来了这样显然是不行的,我们需要更加自动化--一条指令,千军万马来相见。我们需要一种高级的生产车间,我们只需要向生产车间提交一个脚本,工厂便能够通过指令自动化生产。这种更为高级的工厂,就是工厂模式的升华 —— 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);
}
}
这时候,一个十分粗糙的容器就诞生了。现在的确很简陋,但不妨碍我们进一步提升他。先着眼现在,看看这个容器如何使用吧!
// 创建一个容器(后面称作超级工厂)
$container = new Container;
// 向该 超级工厂 添加 钢铁侠 的生产脚本
$container->bind('IronMan', function($container, $moduleName) {
return new IronMan($container->make($moduleName));
});
// 向该 超级工厂 添加 能力模组 的生产脚本
$container->bind('xpower', function($container) {
return new XPower;
});
////
////// 同上
////$container->bind('ultrabomb', function($container) {
//// return new UltraBomb;
////});
// ****************** 华丽丽的分割线 **********************
// 开始启动生产
$superman_1 = $container->make('IronMan', ['xpower']);
$superman_1->test();
//$superman_2 = $container->make('IronMan', ['ultrabomb']);
//$superman_3 = $container->make('IronMan', ['xpower']);
// ...随意添加
看到没?通过最初的 绑定bind
操作,我们向 超级工厂 注册了一些生产脚本,这些生产脚本在生产指令下达之时便会执行。发现没有?我们彻底的解除了 超人 与 超能力模组 的依赖关系,更重要的是,容器类也丝毫没有和他们产生任何依赖!我们通过注册、绑定的方式向容器中添加一段可以被执行的回调(可以是匿名函数、非匿名函数、类的方法)作为生产一个类的实例的 脚本 ,只有在真正的 生产make
操作被调用执行时,才会触发。
这样一种方式,使得我们更容易在创建一个实例的同时解决其依赖关系,并且更加灵活。当有新的需求,只需另外绑定一个“生产脚本”即可。
实际上,真正的 IoC 容器更为高级。我们现在的例子中,还是需要手动提供超人所需要的模组参数,但真正的 IoC 容器会根据类的依赖需求,自动在注册、绑定的一堆实例中搜寻符合的依赖需求,并自动注入到构造函数参数中去。Laravel 框架的服务容器正是这么做的。实现这种功能其实理论上并不麻烦,但我并不会在本文中写出,因为……我懒得写。
不过我告诉大家,这种自动搜寻依赖需求的功能,是通过 反射(Reflection) 实现的,恰好的,php 完美的支持反射机制!关于反射,php 官方文档有详细的资料,并且中文翻译基本覆盖,足够学习和研究!
php.net/manual/zh/book.reflection.p...
原文链接: www.insp.top/learn-laravel-contain... ;
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: