Jakob Jenkov 2015-07-05
自 2007–2008 年以来,“依赖注入”(Dependency Injection, DI)一直是热门话题。关于依赖注入,已经有很多文章和论述,但我认为还有更多值得探讨的内容。本系列教程将解释并深入探讨依赖注入及其相关概念。
依赖注入详解
依赖注入是一种对象配置方式,其中对象的字段和协作者(collaborators)由外部实体进行设置。换句话说,对象是由外部实体来配置的。依赖注入是让对象自行配置自身的一种替代方案。
这听起来可能有些抽象,让我们来看一个简单的例子:
public class MyDao {
protected DataSource dataSource = new DataSourceImpl("driver", "url", "user", "password");
// 数据访问方法...
public Person readPerson(int primaryKey) { ... }
}
这个 DAO(数据访问对象)类 MyDao 需要一个 javax.sql.DataSource 实例,以便获取数据库连接。这些数据库连接用于从数据库读取或写入数据,比如 Person 对象。
注意,MyDao 类自己实例化了一个 DataSourceImpl 作为所需的 DataSource。这意味着 MyDao “依赖”于 DataSource 接口及其实现。没有 DataSource 的实现,它就无法完成工作,因此 MyDao 对 DataSource 接口及其某个具体实现存在“依赖”。
由于 MyDao 自己实例化了 DataSourceImpl,我们说它“自行满足了自己的依赖”。当一个类自行满足依赖时,它也就自动依赖于它用来满足依赖的那些类。在这个例子中,MyDao 不仅依赖于 DataSourceImpl,还依赖于传递给 DataSourceImpl 构造函数的四个硬编码字符串值。如果不修改代码,你就无法更改这四个字符串的值,也无法使用不同的 DataSource 实现。
显而易见,当一个类自行满足依赖时,它在这些依赖上就变得非常不灵活。这意味着如果你需要更改依赖项,就必须修改代码。例如,如果你需要切换数据库,就必须修改 MyDao 类。如果你有很多类似这样实现的 DAO 类,那么你必须全部修改。此外,你无法使用模拟(mock)的 DataSource 实现对 MyDao 进行单元测试,只能使用 DataSourceImpl。显然,这不是一个好的设计。
改进设计:通过构造函数注入
让我们稍微改进一下设计:
public class MyDao {
protected DataSource dataSource = null;
public MyDao(String driver, String url, String user, String password) {
this.dataSource = new DataSourceImpl(driver, url, user, password);
}
// 数据访问方法...
public Person readPerson(int primaryKey) { ... }
}
注意,DataSourceImpl 的实例化被移到了构造函数中。构造函数接收四个参数,即 DataSourceImpl 所需的四个值。虽然 MyDao 仍然依赖这四个值,但它不再自己满足这些依赖,而是由创建 MyDao 实例的外部类提供这些值。这些值被“注入”到 MyDao 的构造函数中——这就是“依赖注入”一词的由来。
现在,你可以在不修改 MyDao 类的情况下,更改其使用的数据库驱动、URL、用户名和密码。
注意:依赖注入不仅限于构造函数。你也可以通过 setter 方法注入依赖,甚至直接注入到公共字段中。
进一步解耦:注入接口而非实现
MyDao 类还可以进一步解耦。目前它仍同时依赖于 DataSource 接口和 DataSourceImpl 类。实际上,它只需要依赖 DataSource 接口即可。我们可以通过向构造函数注入一个 DataSource 实例(而不是四个字符串)来实现这一点:
public class MyDao {
protected DataSource dataSource = null;
public MyDao(DataSource dataSource) {
this.dataSource = dataSource;
}
// 数据访问方法...
public Person readPerson(int primaryKey) { ... }
}
现在,MyDao 不再依赖 DataSourceImpl 类,也不再依赖其构造函数所需的四个字符串。你可以将任意 DataSource 实现注入到 MyDao 的构造函数中。
依赖注入链(Dependency Injection Chaining)
前面的例子做了简化,并未完全展现依赖注入的全部优势。你可能会质疑:依赖只是从 MyDao 转移到了使用它的客户端。客户端现在必须知道某个 DataSource 实现,才能将其注入到 MyDao 的构造函数中。例如:
public class MyBizComponent {
public void changePersonStatus(Person person, String status) {
MyDao dao = new MyDao(
new DataSourceImpl("driver", "url", "user", "password")
);
Person person = dao.readPerson(person.getId());
person.setStatus(status);
dao.update(person);
}
}
可以看到,MyBizComponent 现在依赖于 DataSourceImpl 类及其构造函数所需的四个字符串。这比让 MyDao 依赖它们更糟糕,因为 MyBizComponent 本身并不直接使用这些类或信息。而且,DataSourceImpl 及其参数属于另一个抽象层——即 DAO 层,位于 MyBizComponent 之下。
解决方案是将依赖注入贯穿整个应用层级。MyBizComponent 应只依赖 MyDao 实例,而不应关心 DataSource。如下所示:
public class MyBizComponent {
protected MyDao dao = null;
public MyBizComponent(MyDao dao) {
this.dao = dao;
}
public void changePersonStatus(Person person, String status) {
Person person = dao.readPerson(person.getId());
person.setStatus(status);
dao.update(person);
}
}
再次通过构造函数注入依赖。现在,MyBizComponent 仅依赖 MyDao 类。如果 MyDao 是一个接口,你甚至可以在不修改 MyBizComponent 的情况下切换其实现。
这种依赖注入模式会一直延续到应用程序的顶层,从底层的数据访问层一直到用户界面。