依赖注入与容器
依赖注入与容器
依赖注入
什么是依赖
依赖
就是不是我自身的却是我需要的
,也就是在ClassA内存在一个ClassB的对象的引用,那么ClassA对ClassB就存在一个依赖。
例如下面的类Boy中用到了一个Girl对象,我们就说类Boy对类Girl有一个依赖。
class Boy {
protected $girl;
public function __construct() {
// 需要个妹子做饭哇
$this->girl = new Girl();
}
}
现在如果我要修改Girl类的初始化方法,如修改为new Girl($name),则修改完Girl的代码还需修改Boy的代码,代码的维护性比较差。
如果代码中之前已经声明了
$tomGirl = new Girl('tom')
,那么很难再Boy中使用该Girl对象,因为Girl的初始化写死在了Boy内部。
什么是依赖注入与控制反转
对于这种问题,我们可以很容易的规避掉,例如:
class Boy {
protected girl;
public function __construct(Girl $girl) {
$this->girl = $girl;
}
}
这个时候边将Boy对Girl的依赖转移到了类定义之外,这样Boy不在主动的依赖
Girl,而是需要在初始化的时候将Girl作为一个参数传入构造函数。像这种依赖不是主动的初始化解决,而是由外部传入依赖解决的方式,我们就可以理解为依赖注入
。这种将依赖的控制权转交出去的行为便是控制反转
。
依赖注入很好的解决了上述的问题:
将依赖与本身解耦。高度耦合的代码难以维护和调试。
方便单元测试,方便模拟数据。
下面举个例子说明一下如何实现解耦:
interface Girl {
public function cook();
}
class LolitaGirl implement Girl {
public function cook() {
echo "萝莉做的饭还需努力";
}
}
class LadyGirl implement Girl {
public function cook() {
echo "女士做的饭很好吃";
}
}
class Boy {
// 我需要个妹子做饭给我吃
protected $girl;
public function __construct(Girl $girl) {
$this->girl = $girl;
}
}
$lolitaGirl = new LolitaGirl();
$ladyGirl = new LadyGirl();
// 我要吃萝莉的饭
$boy = new Boy($lolitaGirl);
// 我要吃女士的饭
$boy = new Boy($ladyGirl);
通过从外部注入依赖的方式,实现很方便的切换要什么样的妹子做什么样的饭~
作为一名Boy,我不仅需要Girl的做的饭还需要Skill去养家,所以Boy就需要修改为:
class Skill {
protected $level;
public function __construct($level) {
$this->level = $level;
}
}
class Boy {
protected $girl;
protected $skill;
public function __construct(Girl $girl, Skill $skill) {
$this->girl = $girl;
$this->skill = $skill;
}
}
// 给Boy一个Girl做饭吃,在拥有一个10000能力值的技能养家
$boy = new Boy(new LadyGirl(), new Skill(10000));
如果Boy经过成长获取新的属性,也就是需要给Boy添加更多的依赖类,如果还是继续通过构造函数传实例对象的话就会使得构造函数尤为的不和谐,并且动态的添加也不可能。
要想解决这个问题,可以使用setter设置依赖,例如:
class Boy {
protected $module;
public __construct() {
$this->module = [];
}
public function __set($name, $value) {
$this->module[$name] = $value;
}
public function __get($name) {
if (array_key_exists($name, $this->module)) {
return $this->module[$name];
}else {
throw new \Exception($name . 'is not exists');
}
}
}
$boy = new Boy();
$boy->ladyGirl = new LadyGirl();
$boy->skill = new Skill(10000);
抽象工厂模式
就像上面的例子,当Boy所依赖的属性越来越多时就会出现很多的setter,你会发现这些setter的行为都是一样的,完全可以抽象出一个工厂
出来帮忙你去set。
class FactoryBoy {
public function createModule($module, $option = []) {
if (class_exists($module)) {
if ([] == $option) {
return new $module();
} else {
return new $module($option);
}
}else {
throw new \Exception('class ' . $module . ' is not exists!');
}
}
}
class Boy {
public function __construct($modules) {
$factory = new FactoryBoy();
foreach($modules as $moduleName => $moduleOption) {
$this->data[] = $factory->createModule($moduleName, $moduleOption);
}
}
}
$boy = new Boy([
LadyGirl::class => '',
Skill::class => 10000,
]);
到现在为止,我们实现了不在Boy内创建依赖对象也能够解决Boy的依赖的问题,知识指定下Boy需要的依赖,由工厂来生产相关的依赖组件。这种将依赖转移到工厂,由工厂来生产模块组件解决依赖。
这个时候的工厂
便有了一点容器
的味道。
容器
这里说的容器便是依赖注入的容器,也是控制反转的容器。他实现的是依赖单元的实例化和注入的过程。我们将上面的工厂在修改优化下,使得他能够绑定依赖并从容器中解析出实例对象。
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('Boy', function($container, $moduleName) {
return new Boy($container->make($moduleName));
});
$container->bind('LadyGirl', function($container) {
return new LadyGirl();
})
// 一个拥有了ladyGirl做饭的Boy
$boy = $container->make('Boy', ['LadyGirl']);
可以看到,我们通过绑定(bind)
的方式向容器中添加一段可以被执行的回调(可以是匿名函数、非匿名函数、类的方法)作为生产一个类的实例的脚本 ,只有在真正的生产(make)
操作被调用执行时,才会触发。
实际上,真正的容器比这个容器更加高级,能够自动的自动的寻找依赖,并自动的完成注入,上面的只是一个粗糙的实现。