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

本文參考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中.

  1. 配置寫死在代碼中
// 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í)際需求.

  1. 依賴注入.

為了解決上述問(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).

  1. 把依賴的對(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è)組件: SomeComponentContainer. 當(dāng)SomeComponent的依賴發(fā)生變化時(shí):

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

這樣一來(lái), SomeComponent的修改會(huì)導(dǎo)致ContainerClient的修改. 換句話說(shuō), 實(shí)際上又回到了當(dāng)初寫死代碼的情形.

  1. 控制反轉(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

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

  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 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末再膳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌臀晃,老刑警劉巖粥帚,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缘滥,死亡現(xiàn)場(chǎng)離奇詭異获讳,居然都是意外死亡诵原,警方通過(guò)查閱死者的電腦和手機(jī)风钻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門顷蟀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人骡技,你說(shuō)我怎么就攤上這事鸣个。” “怎么了?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵囤萤,是天一觀的道長(zhǎng)昼窗。 經(jīng)常有香客問(wèn)我,道長(zhǎng)阁将,這世上最難降的妖魔是什么膏秫? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮做盅,結(jié)果婚禮上缤削,老公的妹妹穿的比我還像新娘。我一直安慰自己吹榴,他們只是感情好亭敢,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著图筹,像睡著了一般帅刀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上远剩,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天扣溺,我揣著相機(jī)與錄音,去河邊找鬼瓜晤。 笑死锥余,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的痢掠。 我是一名探鬼主播驱犹,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼足画!你這毒婦竟也來(lái)了雄驹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤淹辞,失蹤者是張志新(化名)和其女友劉穎医舆,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體象缀,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡彬向,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了攻冷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娃胆。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖等曼,靈堂內(nèi)的尸體忽然破棺而出里烦,到底是詐尸還是另有隱情凿蒜,我是刑警寧澤,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布胁黑,位于F島的核電站废封,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏丧蘸。R本人自食惡果不足惜漂洋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望力喷。 院中可真熱鬧刽漂,春花似錦、人聲如沸弟孟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)拂募。三九已至庭猩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間陈症,已是汗流浹背蔼水。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留录肯,地道東北人趴腋。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像嘁信,于是被迫代替她去往敵國(guó)和親于样。 傳聞我的和親對(duì)象是個(gè)殘疾皇子疏叨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容