首先我们抛开那玄乎其神的定义,我们先从一个场景入手。
假设我们有两个类:class A 和 class B,以下简称A和B;
现在我们要使用A,而A依赖了B,A的构造如下:
class A
{
private $b;
public function __construct()
{
$this->b = new B;
}
}
我们知道这种写法十分不利于后期维护,一旦B发生了变化,A不得不修改。
解决这种依赖有很多种方式,但是这里只提一种:
class A
{
private $b;
public function setB($b)
{
$this->b = $b;
}
}
现在至少A的内部没有关于B的代码了,我们将依赖转到外部去,现在创建A实例需要这样:
$a = new A;
$a->setB(new B);
这样写多不方便啊,于是我们再添加一个工厂类,专门封装这样的逻辑
class Factory
{
public static function getA()
{
$a = new A;
$a->setB(new B);
return $a;
}
}
$a = Factory::getA();
这样通过一个工厂来维护类的产生,统一管理,方便维护。
然而 A::setB() 方法参数并没有限制类型,也就是可以任意传入一个参数,这显然缺少约束,所以需要接口,接口一般是事先设计好的约定,不会变更,这里我们假设设定一个接口
interface BInterface{}
B需要实现接口
class B implements BInterface{}
改造一下A
class A
{
private $b;
public function __construct(BInterface $b)
{
$this->b = $b;
}
}
工厂
class Factory
{
public static function getA()
{
$a = new A(new B);
return $a;
}
}
$a = Factory::getA();
前面说了,接口是不会随意变化的,所以我们可以放心的在构造函数中要求传入b,不管B是什么具体实现,只要符合接口就不会影响A。
这里为什么还要用工厂呢?有人说我直接在业务代码里 new A 就可以了啊,仔细想想,A本身如果发生变化了呢?哪天构造函数变成了
function __construct(C $c,B $b){}
所以工厂统一了实例化过程,一旦A变化,只要改工厂类即可。
那么,工厂类本身体积越来越大怎么办?同样难以维护!好,接下来就是见证奇迹的时刻,哦不,是掀开依赖注入面纱的时刻。
现在我们假设,有没有一个万能的工厂方法,可以帮我们生产不同的类,并自动解决依赖问题?
现在我们创建一个类来模拟这个功能
class Di
{
function make($class){}
}
对应的使用过程
$di = new Di;
$a = $di->make(A::class);//参数是php5.5的语法,可以获取一个类的名字,不容易出错,传统的可以用字符串'A'替代
$a->getB();//获取 A::$b 属性
ok,这就完啦,我们不用关系它内部怎么处理的,我只知道我拿到了一个A的对象啦,并且跟上面的工厂方法一样,已经帮我传入了B对象,更惊奇的是,这个方法并不仅限于A类,可以对B、C、D、……任意的类使用。
$c = $di->make(C::class);
$d = $di->make(D::class);
//...
这个特性叫依赖注入,di可以自动分析目标类的依赖,并从其他地方解决这些依赖,这时候为了方便管理,引出了一个容器的概念,我们事先往容器中注册一些类或对象,分别有个别名,这样每次make就不需要知道具体类的名字,又一次解耦。
比如我想获得一个redis的实例,我不用关心这个redis具体是哪个类,我只要告诉di我想要名为'redis'的对象。
$di->bind('redis',MyRedis::class);//事先注册redis服务
$redis = $di->make('redis');
$redis->get('key');//可以使用redis对象了
di会自动帮我们实例化 MyRedis,假设MyRedis依赖类B接口的某个类
class MyRedis
{
public function __construct(BInterface $b){}
}
那么这时候di也能从容器中查找实现这个接口的类,并注入到MyRedis中,是不是很强大?
$di->bind('BInterface',B::class);
$di->bind('redis',MyRedis::class);
$di->make('redis');//相当于 new MyRedis($di->make('BInterface'))
这样就很灵活了,依赖的那个类也可能依赖别的类,而只要把一切类放在容器中,由di自动去解决依赖,我们只管 bind 和 make 就好了,不用写那么多工厂方法。
而类本身也不会依赖di,di我觉得就是一个全局的依赖管理器和工厂的结合。
至于PHP的di有哪些实现,太多了,你只要在github上搜 "php di",或者直接去symfony网上找一个叫做"DependencyInjection"的组件,或者去laravel手册中查阅服务容器相关的章节,有兴趣可以研究他们的代码,核心是借助反射。
我描述的内容比较简单,事实上 依赖注入/控制反转/服务容器 是一个东西罢了,思路也很清晰,只是包装的复杂程度不同罢了。