編程思想: 控制反轉(Inversion of Control - IoC)

本文參考PHP開發(fā)框架phalcon的文檔[1]. 它從一個簡單的例子出發(fā), 描述了編碼中遇到的一系列問題, 然后一步步去解決, 最后得到一個解決方案. 在這個例子中我們了解到:

  • 一種設計模式: 依賴注入(Dependency Injection)
  • 控制反轉是什么?
  • 控制反轉是為了解決什么問題?

在這個例子中, 我們要寫一個類SomeComponent來實現(xiàn)某個功能. 由于它依賴連接數(shù)據庫, 我們把對數(shù)據庫的連接以及相關操作寫在方法doDbTask中.

  1. 配置寫死在代碼中
// SomeComponent.java
public class SomeComponent {

    public void doDbTask() throws Exception {
        // 數(shù)據庫連接的配置寫死在代碼中
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection(
                "url",
                "user",
                "password");
        // ...
    }
}

代碼寫死導致我們不能更改連接的配置, 顯然無法滿足實際需求.

  1. 依賴注入.

為了解決上述問題, 我們可以把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ù)據庫連接, 造成資源浪費.

  1. 把依賴的對象放入容器.
// 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實際上依賴兩個組件: SomeComponentContainer. 當SomeComponent的依賴發(fā)生變化時:

  1. 開發(fā)者需要修改SomeComponent的依賴, 并把依賴的類在Container中實例化.
  2. 由于SomeComponent的構造函數(shù)發(fā)生了變化, Client中用來實例化SomeComponent對象的代碼需要做相應的修改.

這樣一來, SomeComponent的修改會導致ContainerClient的修改. 換句話說, 實際上又回到了當初寫死代碼的情形.

  1. 控制反轉

為了克服上面的問題, 一個解決思路是把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

  1. 控制反轉試圖解決的是在 同一個開發(fā)框架下, 模塊之間的解耦和復用的問題.
  2. 框架的出現(xiàn)或多或少是為了解決開發(fā)語言在某些方面的缺陷. 有些編程語言(例如Python)就能自然做到解耦和復用, 而無需依賴額外的框架(想想為什么).

  1. https://docs.phalcon.io/3.4/en/di ?

  2. https://en.wikipedia.org/wiki/Dependency_injection ?

  3. https://en.wikipedia.org/wiki/Factory_method_pattern ?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容