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