控制反转(Inversion of Control,英文缩写为 IoC)是一个重要的面向对象编程的法则来削减计算机程序的耦合问题。 控制反转一般分为两种类型,依赖注入(Dependency Injection,简称 DI)和依赖查找(Dependency Lookup)。依赖注入应用比较广泛。没接触过的同学,看了上面的介绍一定会想这是什么玩意儿,好像无比 high big up。下面笔者就把自己的理解来告诉大家。

依赖注入

假设我们这里有一个类 A,可以看出在构造方法中引入了数据库类,并且实例化它存入到自身属性,方便其他方法调用。看上去我们实现了想要的功能,但是这是一个噩梦的开始。A,B,C,D,E 越来越多的类需要数据库类,如果都这么写的话,万一有一天数据库密码改了或者 db 类发生变化了,岂不是要回头修改所有类文件?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class A {

    private $db;

    public function __construct()
    {
        include "./Lib/Db.php";
        $this->db = new Db("localhost","root","123456","test");
    }

    public function getList()
    {
        $this->db->query("......");
    }

    more ...
}

好吧,为了解决这个问题,下面的 工厂模式 出现了,我们创建了一个 Factory 方法,并通过 Factory::getDb() 方法来获得 db 组件的实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Factory {

    public static function getDb(){

        include "./Lib/Db.php";

        return new Db("localhost","root","123456","test");

    }
    ...
}

class A {

    private $db;

    function __construct(){
        $this->db = Factory::getDb();
    }

    function getList(){
        $this->db->query("......");
    }
    ...
}

现在出现数据库密码变了等情况,直接去修改工厂类里面的 getDb 方法就行了。实际上你由原来的直接与 Db 类的耦合变为了和 Factory 工厂类的耦合。但是突然有一天工厂方法需要改名,或者 getDb 方法需要改名,你又怎么办?当然这种需求其实还是很操蛋的,但有时候确实存在这种情况。一种解决办法是,我们不在 A 类内部实例化数据库组件,转移到外部,从外部注入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class A {

    private $db;

    //从外部注入 db 连接(构造方法注入也可以)
    function setDb($connection){
        $this->db = $connection;
    }

    function getList(){
        $this->db->query("......");
    }
    ...
}

//调用
$a = new A();
$a->setDb(Factory::getDb());//注入 db 连接
$a->getList();

这样一来,A 类完全与外部类解除耦合了,你可以看到 A 类里面已经没有工厂方法或 Db 类的身影了。我们通过从外部调用工厂类的 setDb 方法,将连接实例直接注入进去。这样 A 完全不用关心 db 连接怎么生成的了,这就叫 依赖注入 。A 类依赖于 Db 类,实现不是在代码内部创建依赖关系,而是让其作为一个参数传递,这使得我们的程序更容易维护,降低程序代码的耦合度,实现一种松耦合。

这还没完,假设 A 类还依赖其他很多类呢?

1
2
3
4
$A->setDb(Factory::getDb());//注入 db 连接
$A->setFile(Factory::getFile());//注入文件处理类
$A->setImage(Factory::getImage());//注入 Image 处理类
...

当然我们还可以创建一个工厂方法 Factory::getA() ,用来处理 A 类的依赖。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Factory {

    public static function getA(){
        $a = new A();
        $a->setDb(Factory::getDb());//注入 db 连接
        $a->setFile(Factory::getFile());//注入文件处理类
        $a->setImage(Factory::getImage());//注入 Image 处理类
        return $a;
    }
}

//调用 A 类变为
$a = Factory::getA();
$a->getList();

似乎完美了,但是怎么感觉又回到了上面第一次用工厂方法时的场景?这确实不是一个好的解决方案,所以又提出了一个概念:容器,又叫做 IoC 容器、DI 容器。他能帮我们管理依赖关系。把所有类绑定到容器中,在调用类时会自动检测依赖关系,自动从容器中加载依赖的类,就不用我们手动写这么多 set 了。

Ioc 容器

先看看效果,假设现在有三个类 A,B,C。之间的关系是 C 依赖于 B,B 依赖于 A。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class A {
    public function say()
    {
        echo 'i am A <br />';
    }
}

class B {
    private $a;
    public function __construct(A $a) //注入 A 的实例
    {
        $this->a = $a;
    }
    public function say()
    {
        $this->a->say();
        echo 'i am B <br />';
    }
}

class C {
    private $b;
    public function __construct(B $b)
    {
        $this->b = $b;
    }
    public function say()
    { 
        $this->b->say();
        echo 'i am C <br />';
    }
}

现在把他们都注册到容器中,假设容器类叫 App。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$app = new App();
$app->bind('a','A'); //将 A 注册到容器中,小写 a 是在容器中的别名,大写 A 是类的全称
$app->bind('b','B');
$app->bind('c','C');

//调用 B 的 say 方法
$b = $app->make('b');
$b->say();

//结果
i am A
i am B

//调用 C 的 say 方法
$c = $app->make('c');
$c->say();

//结果
i am A
i am B
i am C

发现没有?我们彻底的解除了类和类的依赖关系,达到了比较高的解耦。更重要的是,容器类也丝毫没有和他们产生任何依赖!实现了控制反转。而且注入容器中的类,只有在 make 时才会实例化它。这是我写的一个容器 demo,主要用到了 PHP 的反射去抓取依赖的类。