什么是依赖注入
导读
这一系列的文章讲述了依赖注入概念和一个轻量级的使用PHP实现。本篇文章为Part 1。
目录
- Part 1: 什么是依赖注入
- Part 2: 你是否需要依赖注入容器(待翻译)
- Part 3: Symfony服务容器介绍(待翻译)
- Part 4: Symfony服务容器: 使用Builder构建服务(待翻译)
- Part 5: Symfony服务容器: 使用XML或YAML描述服务(待翻译)
- Part 6: 速度的需要(待翻译)
正文
今天我先暂时不会讨论容易,而是首先介绍一下依赖注入的概念,并通过实例来演示一下它所要解决的问题以及开发者可以获得的益处。如果你已经了解了依赖注入的概念可以放心的跳过这边文章而等待下一篇文章。
据我所知,依赖注入可能是最简单的设计模式之一了。并且你很可能已经在用依赖注入了。然而它仍然是最难以解释的概念之一。我猜测大概是缺乏合适的示例来介绍依赖注入这个概念。我要尝试补充一些示例来使PHP社区更好。由于PHP是一门主要用于web开发的语言,让我们以一个简单的web为例。
为了克服HTTP协议的无状态性,web应用需要一个存储每一次web请求的用户信息。这通过使用cookie当然很容易实现,或者更好些的,使用PHP内置的会话机制:
$_SESSION['language'] = 'fr';
上边的代码在会话变量中存储了用户语言。从而,对该用户的所有后续请求,通过全局变量$_SESSION数组可以获取到语言信息。
$user_language = $_SESSION['language'];
由于依赖注入只在面向对象编程领域有意义,我们假设已经有了一个SessionStorage类来封装PHP的会话机制:
class SessionStorage
{
function __construct($cookieName = 'PHP_SESS_ID')
{
session_name($cookieName);
session_start();
}
function set($key, $value)
{
$_SESSION[$key] = $value;
}
function get($key)
{
return $_SESSION[$key];
}
// ...
}
以及一个User类提供了更高级别的接口:
class User
{
protected $storage;
function __construct()
{
$this->storage = new SessionStorage();
}
function setLanguage($language)
{
$this->storage->set('language', $language);
}
function getLanguage()
{
return $this->storage->get('language');
}
// ...
}
这些类很简单,使用User类也是很简单:
$user = new User();
$user->setLanguage('fr');
$user_language = $user->getLanguage();
这一切都很好,直到你需要更多的灵活性。如果你想要改变会话实例cookie的名字该怎么办?这里列出了一些可能的方案:
-
在SessionStorage构造函数中硬编码User类中使用的名字:
class User
{
function __construct()
{
$this->storage = new SessionStorage('SESSION_ID');
}
// ...
}
-
在User类外定义个常量:
class User { function __construct() { $this->storage = new SessionStorage(STORAGE_SESSION_NAME); } // ... } define('STORAGE_SESSION_NAME', 'SESSION_ID');
-
为User构造函数添加会话名字的参数
class User { function __construct($sessionName) { $this->storage = new SessionStorage($sessionName); } // ... } $user = new User('SESSION_ID');
-
为存储类增加一个选项数组:
class User { function __construct($storageOptions) { $this->storage = new SessionStorage($storageOptions['session_name']); } // ... } $user = new User(array('session_name' => 'SESSION_ID'));
所有这些选项都很糟糕。在User类里硬编码会话名字没有真正地解决问题,因为当你回头想要修改这个名字时,必须得再次修改User类。使用一个常量同样不是一个好主意,因为User类依赖一个常量设置。会话名作为参数传入或者提供一个选项数组可能是其中最好的方式了,当仍然让人觉得不太好。User构造函数的参数里掺杂了跟当前对象没有关系的东西。
而另一个问题更难解决:如果我想要修改SessionStorage类呢?例如,为了测试想要将之替换为一个模拟对象。或者你打算在数据库表中或内存中存储会话信息。除非你改变User类,否则当前的实现很难做到这一点。
依赖注入正式登场。我们将SessionStorgae实例通过User构造函数的参数注入到其实例中,而不是在User类中去创建SessionStorage对象实例了:
class User
{
function __construct($storage)
{
$this->storage = $storage;
}
// ...
}
这就是依赖注入。没有更多的了!现在使用User类,需要你首先创建一个SessionStorage对象实例:
$storage = new SessionStorage('SESSION_ID');
$user = new User($storage);
现在配置一个会话存储对象是非常容易的,而且替换一个会话存储类也很容易。由于更好多关注分离,什么都变成了可能的,而不用修改User类。
Pico容器网站这样描述依赖注入:
“依赖注入是一个组件,它通过类的构造函数,方法或者直接作为成员变量来提供类的依赖。”
正如其他的设计模式,依赖注入也有一些反模式的地方。Pico容器网站列出了一些。
构造函数注入不是唯一的注入方式:
-
构造函数注入
class User { function __construct($storage) { $this->storage = $storage; } // ... }
-
设置器注入
class User { function setSessionStorage($storage) { $this->storage = $storage; } // ... }
-
属性注入
class User { public $sessionStorage; } $user->sessionStorage = $storage;
首要原则是,正如我们的示例,构造函数注入最好用来注入必须的依赖;设置器注入则最好用来注入可选的依赖,就像示例中的缓存对象。
当前大多数现代的PHP框架都通过重度使用依赖注入提供了一组解耦但相关的组件:
// symfony: A constructor injection example
$dispatcher = new sfEventDispatcher();
$storage = new sfMySQLSessionStorage(array('database' => 'session', 'db_table' => 'session'));
$user = new sfUser($dispatcher, $storage, array('default_culture' => 'en'));
// Zend Framework: A setter injection example
$transport = new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
'auth' => 'login',
'username' => 'foo',
'password' => 'bar',
'ssl' => 'ssl',
'port' => 465,
));
$mailer = new Zend_Mail();
$mailer->setDefaultTransport($transport);
如果你有兴趣了解更多的依赖注入,我非常推荐你阅读Martain Flower的文章或者Jeff优秀的演讲。你也可以看一下我的这个演讲(注:翻译时原链接已不可打开),这是去年我讲的关于依赖注入,里边有这篇文章中所述示例的更多细节。
今天就这些内容。我希望你能对依赖注入这个概念有了更好的理解。在这个系列接下来的文章中,我将要讨论依赖注入容器。
转载必须注明出处: http://zengfeng.info/2016/04/什么是依赖注入