本文參考PHP開發(fā)框架phalcon的文檔[1]. 它從一個簡單的例子出發(fā), 描述了編碼中遇到的一系列問題, 然后一步步去解決, 最后得到一個解決方案. 在這個例子中我們了解到:
- 一種設計模式: 依賴注入(Dependency Injection)
- 控制反轉是什么?
- 控制反轉是為了解決什么問題?
在這個例子中, 我們要寫一個類SomeComponent來實現(xiàn)某個功能. 由于它依賴連接數(shù)據庫, 我們把對數(shù)據庫的連接以及相關操作寫在方法doDbTask中.
- 配置寫死在代碼中
// SomeComponent.java
public class SomeComponent {
public void doDbTask() throws Exception {
// 數(shù)據庫連接的配置寫死在代碼中
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"url",
"user",
"password");
// ...
}
}
代碼寫死導致我們不能更改連接的配置, 顯然無法滿足實際需求.
- 依賴注入.
為了解決上述問題, 我們可以把connection對象注入到SomeComponent的實例. 一種常用的方式是把依賴的對象當作SomeComponent的構造函數(shù)的參數(shù), 稱為構造器注入. (其它注入方式可以參考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();
}
}
現(xiàn)在假設很多模塊都要使用SomeComponent, 因此每個模塊都需要初始化一個Connection的實例. 這樣不僅麻煩, 而且不能復用數(shù)據庫連接, 造成資源浪費.
- 把依賴的對象放入容器.
// Container.java
public class Container {
private static Connection connection;
/**
* 創(chuàng)建數(shù)據庫連接.
*/
private static void createConnection() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection(
"url",
"user",
"password");
}
/**
* 獲取已有的數(shù)據庫連接,
* 不存在則創(chuàng)建新的連接.
*/
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();
}
}
現(xiàn)在假設SomeComponent依賴很多模塊, 除了Connection之外, 它還依賴FileSystem, HttpClient, HttpCookie. 按照上面的方法(工廠模式[3]), 首先要把依賴的對象作為SomeComponent的構造函器參數(shù).
// 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;
/**
* 創(chuàng)建數(shù)據庫連接.
*/
private static void createConnection() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection(
"url",
"user",
"password");
}
/**
* 獲取已有的數(shù)據庫連接,
* 不存在則創(chuàng)建新的連接.
*/
public static Connection getConnection() throws Exception {
if(connection == null) createConnection();
return connection;
}
/**
* 實例化FileSystem對象.
*/
public static void createFileSystem() {
// ...
}
/**
* 獲取FileSystem實例,
* 不存在則創(chuàng)建新的實例.
*/
public static FileSystem getFileSystem() {
if(fileSystem == null) createFileSystem();
return fileSystem;
}
/**
* 實例化HttpClient對象.
*/
public static void createHttpClient() {
// ...
}
/**
* 獲取HttpClient實例,
* 不存在則創(chuàng)建新的實例.
*/
public static HttpClient getHttpClient() {
if(httpClient == null) createHttpClient();
return httpClient;
}
/**
* 實例化HttpCookie對象.
*/
public static void createHttpCookie() {
// ...
}
/**
* 獲取HttpCookie實例,
* 不存在則創(chuàng)建新的實例.
*/
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的依賴發(fā)生變化時:
- 開發(fā)者需要修改
SomeComponent的依賴, 并把依賴的類在Container中實例化. - 由于
SomeComponent的構造函數(shù)發(fā)生了變化,Client中用來實例化SomeComponent對象的代碼需要做相應的修改.
這樣一來, SomeComponent的修改會導致Container和Client的修改. 換句話說, 實際上又回到了當初寫死代碼的情形.
- 控制反轉
為了克服上面的問題, 一個解決思路是把Container的維護工作交給框架(例如Java的Spring, Php的Phalcon, JS的AngularX)來完成, 即通過一些配置使得框架能 發(fā)現(xiàn) SomeComponent的依賴對象. 當SomeComponent需要使用這些對象的時候由框架來完成實例化的工作. 這樣一來, 當SomeComponent的依賴發(fā)生變化時, 開發(fā)者只需要修改SomeComponent和相關依賴的配置, 而所有依賴SomeComponent的應用程序不需要做修改. 這種思路被稱為 控制反轉, 即依賴對象的 控制權 (即對象的生成和銷毀)從開發(fā)者手上轉移到框架.
以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
- 控制反轉試圖解決的是在 同一個開發(fā)框架下, 模塊之間的解耦和復用的問題.
- 框架的出現(xiàn)或多或少是為了解決開發(fā)語言在某些方面的缺陷. 有些編程語言(例如Python)就能自然做到解耦和復用, 而無需依賴額外的框架(想想為什么).