面向对象的一条准则就是将类与类之间的依赖关系解耦,这并不是什么新概念。Dependency Injection(DI)的目的也是一样,引用《implementing laravel》的一句原文:
Dependency Injection is the act of adding (injecting) any dependencies into a class, rather than instantiating them somewhere within the class code itself.
举一个例子来区分这两种情况:
class NoDI { function __construct() { $this->inner_class = new InnerClass(); } }
class DI
{
function __construct(SomeInterface $implementation)
{
$this->inner_class = $implementation;
}
}
上面前一种情况下,如果inner_class的实现发生变化,例如需要重写一个NewInnerClass(),那么需要在NoDI中修改这行代码。而后一种情况下,由于inner_class是从外部传入的,所以DI类本身不需要发生任何变化。
这个道理并不新颖,所以乍一眼看来这没有什么有意思的东西。如果无论什么情况都需要为类额外写一个interface,反倒是一种负担。另外,如果类之间的依赖关系比较复杂,使用起来也不方便,例如:类A依赖类B,类B又依赖类C,那么如果要获取一个A的对象,需要先得到B的对象,进一步递推到C的对象。
为了减少这种重复的实例化过程,Laravel提供了Container。实际上一个Laravel的APP对象本身就继承自Container。还是以A、B、C之间的依赖关系为例:
interface B_Interface {}
interface C_Interface {}
class A {
function __construct(B_Interface $b){}
}
class B implements B_Interface {
function __construct(C_Interface $c){}
}
class C implements C_Interface {
function __construct(){}
}
在没有Container的情况下,会像下面一样来实例化A对象:
$c = new C();
$b = new B($c);
$a = new A($b);
试想我们每次获取A对象都需要写这样的3行代码。在面向对象的开发中,类之间的依赖关系是非常复杂的,这使得这样实例化变得不可行。这或许是许多人更加倾向于面向过程来开发(即使他们的代码里也充满了类定义)。
有了Container以后这样的情况得到了很大的改善。如下面所示:
APP::bind(‘B_Interface’, ‘B’);
APP::bind(‘C_Interface’, function(){
return new C();
});
$a = APP::make(‘A’);
在Laravel中,上面的代码会这样执行:
- 尝试实例化类A,发现其依赖B_Interface
- 尝试找到实现B_Interface的类,发现已注册的B_Interface指向B类
- 尝试实例化类B,发现其依赖C_Interface
- 尝试找到实现C_Interface的类,发现已注册的C_Interface执行一个closure,返回一个C类对象
- 按照上面相反的顺序依次获得C、B、A对象
Laravel自动完成上面这样的递归式实例化过程。在PHP中,只要利用反射就能做到这一点。也许有人会说,上面这样的代码岂止3行,比前面的实现更加麻烦了。但是,在真实的场景下,两个bind语句是由服务提供方来实现的,而且只要做一次,在全局都能使用。所以,其实只需要最后一句话就够了。
上面的例子里,生成实现B_Interface的类的时候用的是:
APP::bind(‘B_Interface’, ‘B’);
但对于C_Interface,用的是closure。这样做是由于B依赖于C_Interface,如果也用closure的话就不方便写了。
Container在Laravel处于绝对核心地位。可以说所有类实例化都是经过Container来完成的,上面举的例子其实只是一种用法而已,在使用Laravel的过程中,由于Container的存在还有非常多技巧。