本文参考PHP开发框架phalcon的文档[1]. 它从一个简单的例子出发, 描述了编码中遇到的一系列问题, 然后一步步去解决, 最后得到一个解决方案. 在这个例子中我们了解到:
- 一种设计模式: 依赖注入(Dependency Injection)
- 控制反转是什么?
- 控制反转是为了解决什么问题?
在这个例子中, 我们要写一个类SomeComponent
来实现某个功能. 由于它依赖连接数据库, 我们把对数据库的连接以及相关操作写在方法doDbTask
中.
- 配置写死在代码中
// SomeComponent.java
public class SomeComponent {
public void doDbTask() throws Exception {
// 数据库连接的配置写死在代码中
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"url",
"user",
"password");
// ...
}
}
代码写死导致我们不能更改连接的配置, 显然无法满足实际需求.
- 依赖注入.
为了解决上述问题, 我们可以把connection
对象注入到SomeComponent
的实例. 一种常用的方式是把依赖的对象当作SomeComponent
的构造函数的参数, 称为构造器注入. (其它注入方式可以参考wiki[2])
// SomeComponent.java
public class SomeComponent {
private Connection connection;
public SomeComponent(Connection connection) {
this.connection = connection;
}
public void doDbTask() throws Exception {
Connection connection = connection;
// ...
}
}
// Client.java
public class Client {
public void useSomeComponent throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"url",
"user",
"password");
SomeComponent someComponent = new SomeComponent(connection);
someComponent.doDbTask();
}
}
现在假设很多模块都要使用SomeComponent
, 因此每个模块都需要初始化一个Connection
的实例. 这样不仅麻烦, 而且不能复用数据库连接, 造成资源浪费.
- 把依赖的对象放入容器.
// Container.java
public class Container {
private static Connection connection;
/**
* 创建数据库连接.
*/
private static void createConnection() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection(
"url",
"user",
"password");
}
/**
* 获取已有的数据库连接,
* 不存在则创建新的连接.
*/
public static Connection getConnection() throws Exception {
if(connection == null) createConnection();
return connection;
}
}
// Client.java
public class Client {
public void useSomeComponent() throws Exception {
// 从容器中获取Connection的实例
SomeComponent someComponent = new SomeComponent(Container.getConnection());
someComponent.doDbTask();
}
}
现在假设SomeComponent
依赖很多模块, 除了Connection
之外, 它还依赖FileSystem
, HttpClient
, HttpCookie
. 按照上面的方法(工厂模式[3]), 首先要把依赖的对象作为SomeComponent
的构造函器参数.
// SomeComponent.java
public class SomeComponent {
private Connection connection;
private FileSystem fileSystem;
private HttpClient httpClient;
private HttpCookie httpCookie;
public SomeComponent(Connection connection, FileSystem fileSystem, HttpClient httpClient, HttpCookie httpCookie) {
this.connection = connection;
this.fileSystem = fileSystem;
this.httpClient = httpClient;
this.httpCookie = httpCookie;
}
public void doDbTask() throws Exception {
Connection conn = connection;
// ...
}
}
其次, 在Container
中实例化新的依赖对象fileSystem
, httpClient
, httpCookie
.
// Container.java
public class Container {
private static Connection connection;
private static FileSystem fileSystem;
private static HttpClient httpClient;
private static HttpCookie httpCookie;
/**
* 创建数据库连接.
*/
private static void createConnection() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection(
"url",
"user",
"password");
}
/**
* 获取已有的数据库连接,
* 不存在则创建新的连接.
*/
public static Connection getConnection() throws Exception {
if(connection == null) createConnection();
return connection;
}
/**
* 实例化FileSystem对象.
*/
public static void createFileSystem() {
// ...
}
/**
* 获取FileSystem实例,
* 不存在则创建新的实例.
*/
public static FileSystem getFileSystem() {
if(fileSystem == null) createFileSystem();
return fileSystem;
}
/**
* 实例化HttpClient对象.
*/
public static void createHttpClient() {
// ...
}
/**
* 获取HttpClient实例,
* 不存在则创建新的实例.
*/
public static HttpClient getHttpClient() {
if(httpClient == null) createHttpClient();
return httpClient;
}
/**
* 实例化HttpCookie对象.
*/
public static void createHttpCookie() {
// ...
}
/**
* 获取HttpCookie实例,
* 不存在则创建新的实例.
*/
public static HttpCookie getHttpCookie() {
if(httpCookie == null) createHttpCookie();
return httpCookie;
}
}
Client可以通过Container
获取Connection
, FileSystem
, HttpClient
, HttpCookie
的实例, 从而初始化SomeCoponent
.
// Client.java
public class Client {
public void useSomeComponent() throws Exception {
// 从容器中获取Connection的实例
SomeComponent someComponent = new SomeComponent(
Container.getConnection(),
Container.getFileSystem(),
Container.getHttpClient(),
Container.getHttpCookie()
);
someComponent.doDbTask();
}
}
等等, 似乎有些问题. Client
实际上依赖两个组件: SomeComponent
和Container
. 当SomeComponent
的依赖发生变化时:
- 开发者需要修改
SomeComponent
的依赖, 并把依赖的类在Container
中实例化. - 由于
SomeComponent
的构造函数发生了变化,Client
中用来实例化SomeComponent
对象的代码需要做相应的修改.
这样一来, SomeComponent
的修改会导致Container
和Client
的修改. 换句话说, 实际上又回到了当初写死代码的情形.
- 控制反转
为了克服上面的问题, 一个解决思路是把Container
的维护工作交给框架(例如Java的Spring, Php的Phalcon, JS的AngularX)来完成, 即通过一些配置使得框架能 发现 SomeComponent
的依赖对象. 当SomeComponent
需要使用这些对象的时候由框架来完成实例化的工作. 这样一来, 当SomeComponent
的依赖发生变化时, 开发者只需要修改SomeComponent
和相关依赖的配置, 而所有依赖SomeComponent
的应用程序不需要做修改. 这种思路被称为 控制反转, 即依赖对象的 控制权 (即对象的生成和销毁)从开发者手上转移到框架.
以Springboot为例, 按框架的形式写好SomeComponent
之后, 如果我们需要使用SomeComponent
, 大致写法如下(详细教程可参考网上的公开教程或使用IntelliJ IDEA构建Spring Boot项目示例):
// Client.java
public class Client{
@Autowired // 由框架自动生成对象
private SomeComponent someComponent;
public Client(SomeComponent someComponent) {
this.someComponent = someComponent;
}
public void useSomeComponent() throws Exception {
someComponent.doDbTask();
}
}
Remark
- 控制反转试图解决的是在 同一个开发框架下, 模块之间的解耦和复用的问题.
- 框架的出现或多或少是为了解决开发语言在某些方面的缺陷. 有些编程语言(例如Python)就能自然做到解耦和复用, 而无需依赖额外的框架(想想为什么).