(译)依赖注入?? 哈??

本文翻译自Dependency Injection: Huh?。 众所周知,Laravel的核心功能包括依赖注入控制反转。在我看过的很多与这些功能相关的文章里,很少有生动形象的解释这两个功能的。这篇文章解决了我的疑惑,虽然这是一篇比较古老的文章了。但是还是想翻译出来和大家分享。

在我们学习写代码的过程中,肯定会碰到有人说一个概念——“依赖注入”,如果你是一个初学者的话, 肯定会对这个概念感到困惑,并且会跳过这一部分。但是,这个功能对于编写可维护性(和可测试性)代码来说是必不可少的一个功能。在这篇文章中,我会将我对依赖注入的理解按照最简单的方法向大家进行阐述。

举个栗子

让我们直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Photo {
/**
* @var PDO The connection to the database
*/
protected $db;

/**
* Construct.
*/
public function __construct()
{
$this->db = DB::getInstance();
}
}

第一次看这段代码我们可能觉得基本还可以。实际上,我们已经写死了一段依赖,即数据库连接。如果我们想换一个连接层呢?或者说,为什么 Photo 对象要与外面的内容有关联呢?事实上,确实如此。这个对象应当只关联 Photo 相关的内容。

基本的理念是你的类应当只处理一件事情。所以 Photo 对象不应当处理数据库的连接。

你可以把对象理解成你家的宠物。你的狗不会自己决定出去散步或者是在公园玩耍。是你决定的!这不是它做决定的地方。

让我们优化这段代码,有两种方式可以达成我们的目标,构造注入和 setter 注入。下面是对应的代码

构造注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Photo {
/**
* @var PDO The connection to the database
*/
protected $db;

/**
* Construct.
* @param PDO $db_conn The database connection
*/
public function __construct($dbConn)
{
$this->db = $dbConn;
}
}

$photo = new Photo($dbConn);

在上面的代码中, 我们在构造方法中将所有的依赖注入。

setter注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Photo {
/**
* @var PDO The connection to the database
*/
protected $db;

public function __construct() {}

/**
* Sets the database connection
* @param PDO $dbConn The connection to the database.
*/
public function setDB($dbConn)
{
$this->db = $dbConn;
}
}

$photo = new Photo;
$photo->setDB($dbConn);

经过我们简单的修改,这个类不在依赖任何的连接。虽然目前看来不是很明显,但是这个技巧能让我们更方便的进行测试,因为我们现在可以在调用setDB的时候mock数据库。

更好的是,如果我们想换一个不同的连接,由于我们使用了依赖注入,这会是一个非常简单的事情。

问题浮现

使用 Setter 注入会有一个问题——会使我们的类依赖越来越难以管理。用户必须时刻记住这个类都用到了哪些依赖以及在使用时要一一进行 set 设置。所以,在我们依赖的内容越来越多的时候,我们就会看到下面的情况:

1
2
3
4
$photo = new Photo;
$photo->setDB($dbConn);
$photo->setConfig($config);
$photo->setResponse($response);

解决方案

解决方案很简单,就是创建一个容器类来替我们处理这些依赖问题。是的,这里就是你经常听到的控制反转(Inversion of Control)——IoC。

定义:在软件工程中,控制反转(IoC)是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引入传递(注入)给它。

—— 上面的定义出自Wikipedia

有几种方式能够实现控制反转,下面分别进行展示说明

方法一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Also frequently called "Container"
class IoC {
/**
* @var PDO The connection to the database
*/
protected $db;

/**
* Create a new instance of Photo and set dependencies.
*/
public static newPhoto()
{
$photo = new Photo;
$photo->setDB(static::$db);
// $photo->setConfig();
// $photo->setResponse();

return $photo;
}
}

$photo = IoC::newPhoto();

现在$photo变量和我们新创建的Photo实例具有同样的效果。这种方法用户就无需记住和设定所需要的依赖,只需要调用newPhoto方法即可。

方法二

与其为每一个对象创建一个方法,不如写一个通用的逻辑:

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
33
34
35
36
37
38
39
40
41
42
43
class IoC {
/**
* @var PDO The connection to the database
*/
protected static $registry = array();

/**
* Add a new resolver to the registry array.
* @param string $name The id
* @param object $resolve Closure that creates instance
* @return void
*/
public static function register($name, Closure $resolve)
{
static::$registry[$name] = $resolve;
}

/**
* Create the instance
* @param string $name The id
* @return mixed
*/
public static function resolve($name)
{
if ( static::registered($name) )
{
$name = static::$registry[$name];
return $name();
}

throw new Exception('Nothing registered with that name, fool.');
}

/**
* Determine whether the id is registered
* @param string $name The id
* @return bool Whether to id exists or not
*/
public static function registered($name)
{
return array_key_exists($name, static::$registry);
}
}

不要被这段代码吓到你,这里的逻辑十分简单。当用户调用IOC::register方法时,IoC 仅仅是设置了一个id,例如photo。和其对应的resolver。resolver的功能是一个匿名函数,其作用是创建实例并设置对应的依赖。

具体我们可以看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
// Add `photo` to the registry array, along with a resolver
IoC::register('photo', function() {
$photo = new Photo;
$photo->setDB('...');
$photo->setConfig('...');

return $photo;
});

// Fetch new photo instance with dependencies set
$photo = IoC::resolve('photo');

我们可以看到,我们不是之间实例化一个类,而是通过IoC容器来进行注册。

1
2
3
4
5
// 之前的写法
$photo = new Photo;

// 现在的写法
$photo = IoC::resolve('photo');

拥抱魔术方法

如果你想缩减容器类的代码,我们可以利用魔术方法__set()__get()

1
2
3
4
5
6
7
8
9
10
11
12
13
class IoC {
protected $registry = array();

public function __set($name, $resolver)
{
$this->registry[$name] = $resolver;
}

public function __get($name)
{
return $this->registry[$name]();
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
$c = new IoC;
$c->mailer = function() {
$m = new Mailer;
// create new instance of mailer
// set creds, etc.

return $m;
};

// Fetch, boy
$mailer = $c->mailer; // mailer instance