1. 程式人生 > >控制容器的反轉和依賴註入模式

控制容器的反轉和依賴註入模式

測試 細節 同時 我們 round 文件中 mes 服務 ram

本文依舊是一篇譯文,寫於作者在開發.net core 半年後的進階學習時刻!

這篇文章很長,一口氣看完得花二十分鐘,大家要做好心理準備!

摘要:Java社群近來掀起了一陣輕量級容器的熱潮,這些容器能夠幫助開發者將來自不同項目的組件組裝成為一個內聚的應用程序。在它們的背後有著同一個模式,這個模式決定了這些容器進行組件裝配的方式。人們用一個大而化之的名字來稱呼這個模式:”控制反轉”(Inversion ofControl,IoC)。在本文中,我將深入探索這個模式的工作原理,給它一個更能描述其特點的名字——”依賴註入”(Dependency Injection),並將其與”服務定位器”(Service Locator)模式作一個比較。不過,這兩者之間的差異並不太重要,更重要的是:應該將組件的配置與使用分離開——兩個模式的目標都是這個。

在企業級Java的世界裏存在一個有趣的現象:有很多人投入很多精力來研究主流J2EE 技術的替代品——自然,這大多發生在open source社群。在很大程度上,這可以看作是開發者對主流J2EE技術的笨重和復雜作出的回應,但其中的確有很多極富創意的想法,的確提供了一些可供選擇的方案。J2EE開發者常遇到的一個問題就是如何組裝不同的程序元素:如果web控制器體系結構和數據庫接口是由不同的團隊所開發的,彼此幾乎一無所知,你應該如何讓它們配合工作?很多框架嘗試過解決這個問題,有幾個框架索性朝這個方向發展,提供了更通用的”組裝各層組件”的方案。這樣的框架通常被稱為”輕量級容器”,PicoContainer和Spring都在此列中。

在這些容器背後,一些有趣的設計原則發揮著作用。這些原則已經超越了特定容器的範疇,甚至已經超越了Java平臺的範疇。在本文中,我就要初步揭示這些原則。我使用的範例是Java代碼,但正如我的大多數文章一樣,這些原則也同樣適用於別的OO環境,特別是.NET。

組件和服務

裝配程序元素,這樣的話題立即將我拖進了一個棘手的術語問題:如何區分”服務”(service)和”組件”(component)?你可以毫不費力地找出關於這兩個詞定義的長篇大論,各種彼此矛盾的定義會讓你感受到我所處的窘境。有鑒於此,對於這兩個遭到了嚴重濫用的詞匯,我將首先說明它們在本文中的用法。

所謂”組件”是指這樣一個軟件單元:它將被作者無法控制的其他應用程序使用,但後者不能對組件進行修改。也就是說,使用一個組件的應用程序不能修改組件的源代碼,但可以通過作者預留的某種途徑對其進行擴展,以改變組件的行為。

服務和組件有某種相似之處:它們都將被外部的應用程序使用。在我看來,兩者之間最大的差異在於:組件是在本地使用的(例如JAR文件、程序集、DLL、或者源碼導入);而服務是要通過同步或異步的遠程接口來遠程使用的(例如web service、消息系統、RPC,或者socket)。

在本文中,我將主要使用”服務”這個詞,但文中的大多數邏輯也同樣適用於本地組件。實際上,為了方便地訪問遠程服務,你往往需要某種本地組件框架。不過,”組件或者服務”這樣一個詞組實在太麻煩了,而且”服務”這個詞當下也很流行,所以本文將用”服務”指代這兩者。

一個簡單的例子

為了更好地說明問題,我要引入一個例子。和我以前用的所有例子一樣,這是一個超級簡單的例子:它非常小,小得有點不夠真實,但足以幫助你看清其中的道理,而不至於陷入真實例子的泥潭中無法自拔。

在這個例子中,我編寫了一個組件,用於提供一份電影清單,清單上列出的影片都是由一位特定的導演執導的。實現這個偉大的功能只需要一個方法:

class MovieLister...

    public Movie[] moviesDirectedBy(String arg)
    {
        List allMovies = finder.findAll();
        for (Iterator it = allMovies.iterator(); it.hasNext();)
        {
            Movie movie = (Movie) it.next();
            if (!movie.getDirector().equals(arg))
            {
                it.remove();
            }

        }
        return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
}

你可以看到,這個功能的實現極其簡單:moviesDirectedBy方法首先請求finder(影片搜尋者)對象(我們稍後會談到這個對象)返回後者所知道的所有影片,然後遍歷finder對象返回的清單,並返回其中由特定的某個導演執導的影片。非常簡單,不過不必擔心,這只是整個例子的腳手架罷了。我們真正想要考察的是finder對象,或者說,如何將MovieLister對象與特定的finder對象連接起來。為什麽我們對這個問題特別感興趣?因為我希望上面這個漂亮的moviesDirectedBy方法完全不依賴於影片的實際存儲方式。所以,這個方法只能引用一個finder對象,而finder對象則必須知道如何對findAll 方法作出回應。為了幫助讀者更清楚地理解,我給finder定義了一個接口:

public interface MovieFinder
{
    List findAll();
}

現在,兩個對象之間沒有什麽耦合關系。但是,當我要實際尋找影片時,就必須涉及到MovieFinder的某個具體子類。在這裏,我把涉及具體子類的代碼放在MovieLister類的構造函數中。

class MovieLister...
    private MovieFinder finder;
    public MovieLister()
    {
        finder = new ColonDelimitedMovieFinder("movies1.txt");
    }

這個實現類的名字就說明:我將要從一個逗號分隔的文本文件中獲得影片列表。你不必操心具體的實現細節,只要設想這樣一個實現類就可以了。如果這個類只由我自己使用,一切都沒問題。但是,如果我的朋友嘆服於這個精彩的功能,也想使用我的程序,那又會怎麽樣呢?如果他們也把影片清單保存在一個逗號分隔的文本文件中,並且也把這個文件命名為” movie1.txt “,那麽一切還是沒問題。如果他們只是給這個文件改改名,我也可以從一個配置文件獲得文件名,這也很容易。但是,如果他們用完全不同的方式——例如SQL 數據庫、XML 文件、web service,或者另一種格式的文本文件——來存儲影片清單呢?在這種情況下,我們需要用另一個類來獲取數據。由於已經定義了MovieFinder接口,我可以不用修改moviesDirectedBy方法。但是,我仍然需要通過某種途徑獲得合適的MovieFinder實現類的實例。

技術分享圖片

圖1:在MovieLister 類中直接創建MovieFinder 實例時的依賴關系

圖1展現了這種情況下的依賴關系:MovieLister類既依賴於MovieFinder接口,也依賴於具體的實現類。我們當然希望MovieLister類只依賴於接口,但我們要如何獲得一個MovieFinder子類的實例呢?

在Patterns of Enterprise Application Architecture一書中,我們把這種情況稱為插件(plugin):MovieFinder的實現類不是在編譯期連入程序之中的,因為我並不知道我的朋友會使用哪個實現類。我們希望MovieLister類能夠與MovieFinder的任何實現類配合工作,並且允許在運行期插入具體的實現類,插入動作完全脫離我(原作者)的控制。這裏的問題就是:如何設計這個連接過程,使MovieLister類在不知道實現類細節的前提下與其實例協同工作。

將這個例子推而廣之,在一個真實的系統中,我們可能有數十個服務和組件。在任何時候,我們總可以對使用組件的情形加以抽象,通過接口與具體的組件交流(如果組件並沒有設計一個接口,也可以通過適配器與之交流)。但是,如果我們希望以不同的方式部署這個系統,就需要用插件機制來處理服務之間的交互過程,這樣我們才可能在不同的部署方案中使用不同的實現。所以,現在的核心問題就是:如何將這些插件組合成一個應用程序?這正是新生的輕量級容器所面臨的主要問題,而它們解決這個問題的手段無一例外地是控制反轉(Inversion of Control)模式。

控制反轉

幾位輕量級容器的作者曾驕傲地對我說:這些容器非常有用,因為它們實現了控制反轉。這樣的說辭讓我深感迷惑:控制反轉是框架所共有的特征,如果僅僅因為使用了控制反轉就認為這些輕量級容器與眾不同,就好象在說我的轎車是與眾不同的,因為它有四個輪子。

問題的關鍵在於:它們反轉了哪方面的控制?我第一次接觸到的控制反轉針對的是用戶界面的主控權。早期的用戶界面是完全由應用程序來控制的,你預先設計一系列命令,例如輸入姓名、輸入地址等,應用程序逐條輸出提示信息,並取回用戶的響應。而在圖形用戶界面環境下,UI框架將負責執行一個主循環,你的應用程序只需為屏幕的各個區域提供事件處理函數即可。在這裏,程序的主控權發生了反轉:從應用程序移到了框架。對於這些新生的容器,它們反轉的是如何定位插件的具體實現。在前面那個簡單的例子中,MovieLister類負責定位MovieFinder的具體實現——它直接實例化後者的一個子類。這樣一來,MovieFinder也就不成其為一個插件了,因為它並不是在運行期插入應用程序中的。而這些輕量級容器則使用了更為靈活的辦法,只要插件遵循一定的規則,一個獨立的組裝模塊就能夠將插件的具體實現註射到應用程序中。因此,我想我們需要給這個模式起一個更能說明其特點的名字——”控制反轉”這個名字太泛了,常常讓人有些迷惑。與多位IoC 愛好者討論之後,我們決定將這個模式叫做”依賴註入”(Dependency Injection)。

下面,我將開始介紹Dependency Injection模式的幾種不同形式。不過,在此之前,我要首先指出:要消除應用程序對插件實現的依賴,依賴註入並不是唯一的選擇,你也可以用ServiceLocator模式獲得同樣的效果。介紹完Dependency Injection模式之後,我也會談到ServiceLocator 模式。

依賴註入的幾種形式

Dependency Injection模式的基本思想是:用一個單獨的對象(裝配器)來獲得MovieFinder的一個合適的實現,並將其實例賦給MovieLister類的一個字段。這樣一來,我們就得到了圖2所示的依賴圖:

技術分享圖片

圖2:引入依賴註入器之後的依賴關系

依賴註入的形式主要有三種,我分別將它們叫做構造函數註入(Constructor Injection)、屬性註入(Setter Injection)和接口註入(Interface Injection)。如果讀過最近關於IoC的一些討論材料,你不難看出:這三種註入形式分別就是type 1 IoC(接口註入)、type 2 IoC(屬性註入)和type 3 IoC(構造函數註入)。我發現數字編號往往比較難記,所以我使用了這裏的命名方式。

使用PicoContainer 進行構造函數註入

首先,我要向讀者展示如何用一個名為PicoContainer的輕量級容器完成依賴註入。之所以從這裏開始,主要是因為我在ThoughtWorks公司的幾個同事在PicoContainer的開發社群中非常活躍——沒錯,也可以說是某種偏袒吧。

PicoContainer通過構造函數來判斷如何將MovieFinder實例註入MovieLister 類。因此,MovieLister類必須聲明一個構造函數,並在其中包含所有需要註入的元素:

class MovieLister...
    public MovieLister(MovieFinder finder)
    {
        this.finder = finder;
    }

MovieFinder實例本身也將由PicoContainer來管理,因此文本文件的名字也可以由容器註入:

class ColonMovieFinder...
    public ColonMovieFinder(String filename)
    {
        this.filename = filename;
    }

隨後,需要告訴PicoContainer:各個接口分別與哪個實現類關聯、將哪個字符串註入MovieFinder組件

private MutablePicoContainer configureContainer()
    {
        MutablePicoContainer pico = new DefaultPicoContainer();
        Parameter[] finderParams = {newConstantParameter("movies1.txt")};
        pico.registerComponentImplementation(MovieFinder.class,ColonMovieFinder.class, finderParams);
        pico.registerComponentImplementation(MovieLister.class);
        return pico;
    }

這段配置代碼通常位於另一個類。對於我們這個例子,使用我的MovieLister 類的朋友需要在自己的設置類中編寫合適的配置代碼。當然,還可以將這些配置信息放在一個單獨的配置文件中,這也是一種常見的做法。你可以編寫一個類來讀取配置文件,然後對容器進行合適的設置。盡管PicoContainer本身並不包含這項功能,但另一個與它關系緊密的項目NanoContainer提供了一些包裝,允許開發者使用XML配置文件保存配置信息。NanoContainer能夠解析XML文件,並對底下的PicoContainer進行配置。這個項目的哲學觀念就是:將配置文件的格式與底下的配置機制分離開。

使用這個容器,你寫出的代碼大概會是這樣:

 public void testWithPico()
    {
        MutablePicoContainer pico = configureContainer();
        MovieLister lister = (MovieLister)pico.getComponentInstance(MovieLister.class);
        Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
        assertEquals("Once Upon a Time in the West",movies[0].getTitle());
    }

盡管在這裏我使用了構造函數註入,實際上PicoContainer也支持屬性註入,不過該項目的開發者更推薦使用構造函數註入。

使用Spring 進行屬性註入

Spring 框架是一個用途廣泛的企業級Java 開發框架,其中包括了針對事務、持久化框架、web應用開發和JDBC等常用功能的抽象。和PicoContainer一樣,它也同時支持構造函數註入和屬性註入,但該項目的開發者更推薦使用設值方法註入——恰好適合這個例子。為了讓MovieLister類接受註入,我需要為它定義一個屬性,該方法接受類型為MovieFinder的參數:

class MovieLister...
    private MovieFinder finder;
    public void setFinder(MovieFinder finder)
    {
        this.finder = finder;
    }

類似地,在MovieFinder的實現類中,我也定義了一個設值方法(Net 中屬性),接受類型為String 的參數:

class ColonMovieFinder...
    public void setFilename(String filename)
    {
        this.filename = filename;
    }

第三步是設定配置文件。Spring 支持多種配置方式,你可以通過XML 文件進行配置,也可以直接在代碼中配置。不過,XML 文件是比較理想的配置方式。

<beans>
    <bean id="MovieLister" class="spring.MovieLister">
        <property name="finder">
            <ref local="MovieFinder"/>
        </property>
    </bean>
    <bean id="MovieFinder" class="spring.ColonMovieFinder">
        <property name="filename">
            <value>movies1.txt</value>
        </property>
    </bean>
</beans>

於是,測試代碼大概就像下面這樣:

public void testWithSpring() throws Exception
    {
        ApplicationContext ctx = newFileSystemXmlApplicationContext("spring.xml");
        MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
        Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
        assertEquals("Once Upon a Time in the West",movies[0].getTitle());
    }

接口註入

除了前面兩種註入技術,還可以在接口中定義需要註入的信息,並通過接口完成註入。Avalon框架就使用了類似的技術。在這裏,我首先用簡單的範例代碼說明它的用法,後面還會有更深入的討論。首先,我需要定義一個接口,組件的註入將通過這個接口進行。在本例中,這個接口的用途是將一個MovieFinder實例註入繼承了該接口的對象。

public interface InjectFinder
{
    void injectFinder(MovieFinder finder);
}

這個接口應該由提供MovieFinder接口的人一並提供。任何想要使用MovieFinder實例的類(例如MovieLister類)都必須實現這個接口。

class MovieLister implements InjectFinder...
    public void injectFinder(MovieFinder finder)
    {
        this.finder = finder;
    }

然後,我使用類似的方法將文件名註入MovieFinder的實現類:

public interface InjectFilename
{
    void injectFilename (String filename);
}

class ColonMovieFinder implements MovieFinder, InjectFilename...
    public void injectFilename(String filename)
    {
        this.filename = filename;
    }

現在,還需要用一些配置代碼將所有的組件實現裝配起來。簡單起見,我直接在代碼中完成配置,並將配置好的MovieLister 對象保存在名為lister的字段中:

class IfaceTester...
    private MovieLister lister;
    private void configureLister()
    {
        ColonMovieFinder finder = new ColonMovieFinder();
        finder.injectFilename("movies1.txt");
        lister = new MovieLister();
        lister.injectFinder(finder);
    }

測試代碼則可以直接使用這個字段:

class IfaceTester...
    public void testIface()
    {
        configureLister();
        Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
        assertEquals("Once Upon a Time in the West",movies[0].getTitle());
    }

翻譯先到這裏,感覺收獲不大,沒有找到我想要的東西,歡迎大家關註我的公眾號:不定時更新文章

技術分享圖片

public interface InjectFilename
{
    void injectFilename (String filename);
}

class ColonMovieFinder implements MovieFinder, InjectFilename...
    public void injectFilename(String filename)
    {
        this.filename = filename;
    }

控制容器的反轉和依賴註入模式