IOC 容器及DI設(shè)計模式解讀

IOC/DI是Spring的核心思想,其在Spring生態(tài)圈中有著舉足輕重的作用,地位崇高,今天我們就來看看IOC/DI緣起何處,路在何方。

在Java社區(qū)中曾掀起一股輕量級容器的潮流,它們致力于將不同項目中的組件裝配到一個高內(nèi)聚的應(yīng)用中。其實,這些容器的底層使用了同一種設(shè)計模式,也就是通常所所說的IOC(控制反轉(zhuǎn))。在下面的內(nèi)容中,將會探究下這種模式(依賴注入)是如何運作的,而且還會與Service Locator(服務(wù)定位器,也是一種IOC實現(xiàn)方式)進行對比。重要的并不是選擇哪種實現(xiàn)方式,DI也好,Service Locator也罷,重要的是配置與使用分離的原則,這個思想才是IOC的精髓所在。

在Java的世界中,有一件非常有趣的現(xiàn)象,出現(xiàn)了很多主流或者說官方JavaEE技術(shù)的替代的開源的組件或活動。在很大程度上,有很多組件是出于對重量級且復(fù)雜的主流JavaEE技術(shù)的本能應(yīng)對,但是也有很多是源自于創(chuàng)造性想法的替代產(chǎn)品。這些組件需要解決一個通用問題:如何將不同的元素連接并裝配在一起。比如,如何將一個web控制器架構(gòu)體系與數(shù)據(jù)庫接口體系組裝在一起,而這兩個組件是由不同團隊開發(fā)的,他們之間彼此并不認識。很多框架已經(jīng)嘗試解決這個問題,他們中有些提供一個通過不同分層來裝配組件的通用能力,這些組件通常被稱為輕量級容器,比如Spring和PicoContainer。

這些容器的底層是很多有意思的設(shè)計原則,接下來會介紹這里面的幾個原則,示例代碼使用Java,但是其中的大部分原則同樣適用于其他OO環(huán)境,尤其是.NET。

Comonents and Services

談到連接元素就不得不說到Component(組件)和Service(服務(wù))這兩個令人困惑的技術(shù)術(shù)語,你可能讀到很多定義這兩個概念的文章,他們很多是冗長且矛盾的。以下就是我目前對這兩個重載術(shù)語的使用。

我使用Component表示提供給其他應(yīng)用程序使用的一系列軟件(不會主動變化),而這個應(yīng)用程序并不受Component開發(fā)者的控制,換句話說Component是個售出的成品(比如手機),用戶直接使用,廠家并不知道用戶如何使用。所謂的不會主動變化,我的意思是應(yīng)用程序不會去改變Component的源碼,但是可以通過Component提供的接口來改變Component的行為,進而實現(xiàn)對Component的擴展。

Service與Component的相近,它被用來提供給為外部程序你使用。我認為它們之間主要的不同在于,Component是本地使用(如jar文件,assembly, dll或 a source import),Service將通過遠程接口遠程使用,同步或異步都可以(比如web Service,messsaging system, RPC or socket)。

我在文章中會大量使用Service,但是其中的大部分邏輯也可以應(yīng)用于本地Component。事實上,通常我們需要使用一些本地Component去訪問遠程的Service。由于Component or Service不易讀寫,所以就簡寫為Service,這樣形式看上去更加“時髦”。

A Naive Example

閑話少說,直接上代碼。為了不至于讓讀者陷入復(fù)雜業(yè)務(wù)場景中,文中都使用極其簡單的示例代碼,但這足以讓你get到其中的精髓。

在下面這個例子中我將編寫一個Component,它有一個提供某個導(dǎo)演的電影的名稱列表。我們使用一個方法就可以實現(xiàn)這個非常實用的功能,代碼片段如下:

public class MovieLister {
    private MovieFindler findler;
    ...
    public Movie[] moviesDirectedBy(String args) {
        List<Movie> allMovies = findler.findAll();
        for (Iterator<Movie> it = allMovies.iterator(); it.hasNext(); ) {
            Movie movie = it.next();
            if (!(args).equals(movie.getDirector())) {
                it.remove();
            }
        }
        return allMovies.toArray(new Movie[allMovies.size()]);
    }
}

這段代碼很簡單,做了3件事:

  1. 從findler對象(稍后介紹)獲取所有電影的列表
  2. 遍歷該列表,刪除不是該導(dǎo)演的電影名稱
  3. 返回剩余的電影名稱列表(即該導(dǎo)演執(zhí)導(dǎo)的電影)

我并不打算修正這段代碼使其看上去更加高級,因為這是本文所要講述的真實的要點,這至關(guān)重要。

這個要點就是這個finder對象,或者說如何將finder對象和lister對象關(guān)聯(lián)起來。

這個例子有趣的地方在于我想要movieDirectedBy這個方法與電影如何被存儲是完全沒有依賴的或者說完全解耦的,所以這個方法引用了finder對象,而finder對象所要做的就是通過finderAll方法將數(shù)據(jù)響應(yīng)給lister對象。我可以通過為finder定義一個接口來實現(xiàn)這一點:

public interface MovieFindler {
    List<Movie> findAll();
}

現(xiàn)在所有的一切都很好的解耦,但是如果要正常使用,還需要頂一個具體類來實現(xiàn)findAll接口,在這個例子中我把創(chuàng)建代碼放在lister類的構(gòu)造函數(shù)中。

public MovieLister(){
    findler = new ColonDelimitedMovieFinder("movies1.txt");
}

這個實現(xiàn)類的名稱源于這是從一個內(nèi)容是逗號分割的文件中獲取數(shù)據(jù),這樣的命名意義明確,是一種良好的命名習(xí)慣。

現(xiàn)在,這個實現(xiàn)類只是我一個人用,這一切都很好。但是,如果我的朋友覺得我寫的這個功能十分誘人并且想要拷貝一份我的代碼呢?如果他們的電影列表也剛好存在用冒號分割的文件中并且文件名也是movie1.txt,這當(dāng)然是極好的。如果他們的文件名不同,我們可以設(shè)計一個屬性文件,將對應(yīng)的文件名配置為在里面,然后從該屬性文件中讀取電影文件名,這樣實現(xiàn)起來也很簡單。但是,如果朋友們使用其他的存儲介質(zhì):比如SQL數(shù)據(jù)庫,XML文件或者使用Web Service,或者使用另一種格式的文本文檔呢?這個時候,我們需要另一個類用于抓取數(shù)據(jù)。因為我們現(xiàn)在是面向接口編程,并不需要去修改moviesDirectedBy方法,但是,我們確實需要采取某種方式來獲取正確的finder實現(xiàn)類對象。

Figure 1: The dependencies using a simple creation in the lister class

從上圖中我么你可以看到示例代碼中的依賴關(guān)系,MovieLister依賴MovieFinlder接口和其實現(xiàn)ColonDelimitedMovieFinder。我們更喜歡讓它只依賴接口,但是怎么才能創(chuàng)建出合適的對象呢?

根據(jù)上述描述,finder的實現(xiàn)不能再編譯期就連接到lister中,因為我并不知道我的朋友使用哪種方式來獲取數(shù)據(jù)。此外,我還希望我的lister可以使用所有的finder實現(xiàn)類而且這個實現(xiàn)將會在以后的某個點插入到lister中。那么,現(xiàn)在的問題就是,我應(yīng)該如何創(chuàng)建這種連接使lister在finder對象實現(xiàn)一無所知的情況下還可以與finder實例對象對話以完成其工作。

將其擴展到一個真實系統(tǒng)中,我們肯能會有很多這樣的服務(wù)或組件。在每一種情況下,我們都可以將使用到的組件抽象出接口,然后通過接口來交互(如果組件不是面向接口開發(fā)的,就使用適配器來進行適配,進而通過適配器進行交互)。但是如果我們想要以不同方式部署系統(tǒng),就需要使用插件來處理與服務(wù)的交互,這樣我們就可以在不同的發(fā)布環(huán)境中使用不同的實現(xiàn)了。

所以這個核心問題就是如何把這些插件裝配到應(yīng)用中呢?這是新型輕量級容器面臨的主要問題之一,通常他們都是通過Inversion of Control(控制反轉(zhuǎn))來實現(xiàn)的。

Inversion of Control

有些容器說自己是很有用的,原因竟然是因為他們實現(xiàn)了IOC,這讓我困惑不已。IOC是框架的共有屬性,所以說輕量級容器因為使用了IOC就變得特殊了,就像說我的汽車之所以不一樣,是因為它有輪子,這聽起來就挺滑稽的。

現(xiàn)在的問題是:什么方面的控制被反轉(zhuǎn)了?當(dāng)我第一次遇到IOC時,它出現(xiàn)在一個用戶交互的控制界面中。早期的用戶界面是由應(yīng)用程序控制的,你將會看到一系列的指令,如“輸入用戶名”,“輸入地址”;這個應(yīng)用程序?qū)o出對應(yīng)的提示,并對你的輸入給出響應(yīng)。使用圖形化的或者基于屏幕的UI框架,框架包含對某事件的主循環(huán)過程,而應(yīng)用程序提供對屏幕各個字段的事件處理器。在這里,對程序的主要控制被轉(zhuǎn)了,從你轉(zhuǎn)移到了框架。簡而言之,用戶只需要提供屬性信息,處理過程交給了UI框架,比如用戶只需要輸入用戶名和密碼,點擊登錄即可,至于其中的用戶名重復(fù)校驗等等交給UI框架自動去完成,或者請求提交這個操作,不是由人敲回車實現(xiàn)交互了,而是交由UI框架去執(zhí)行這個交互過程。

對于這種新型的容器,反轉(zhuǎn)指的是如何查找一個接口實現(xiàn)。在上面的小例子中,lister對象通過直接實例化finder對象實現(xiàn)查找。這樣的話,finder對象就不可插拔了。這些容器要求接口使用者遵循一些規(guī)定,從而可以將一些分離的模塊實現(xiàn)注入到lister對象中。

因此,我們需要一個更加具體的名字來命名這個模式。IOC是一個太通用的術(shù)語,這讓人感到困惑。最后經(jīng)過IOC擁護者的大量討論,最后決定使用Dependency Injection(依賴注入)這個名字來命名該模式。

接下來我們要講的是依賴注入的各種實現(xiàn)形式,但是我要指出的是這并不是從應(yīng)用程序中移除依賴轉(zhuǎn)換為插件式的實現(xiàn)的唯一方式。另外一種可以實現(xiàn)這一點的是Service Locator,在介紹完DI之后,我將會繼續(xù)介紹Service Locator。

Forms of Dependency Injection

依賴注入的基本思想是有一個單獨的對象(一個裝配器),它可以從finder接口的實現(xiàn)中選擇一個合適的對象填充從到lister對象的finder屬性中,依賴圖如下:

主要有三種注入方式:Contructor Injection(構(gòu)造器注入),Setter Injection(set方法注入),Interface Injection(接口注入)。如果你以前閱讀過IOC的介紹,你可能會聽說過type 1控制反轉(zhuǎn)(interface injection),type 2控制反轉(zhuǎn)(setter injection)和type 3控制反轉(zhuǎn)(constuctor injection)。數(shù)字類型的名字不易區(qū)分,所以我這里使用更有表征意義的名字。

Constructor Injection with Spring

Spring是一個應(yīng)用廣泛的企業(yè)級框架,它包括事務(wù)抽象層,持久層框架,web應(yīng)用開發(fā)和JDBC等。它支持構(gòu)造方法和setter方法注入。

下面介紹一下Spring使用構(gòu)造方法來實現(xiàn)將finder實現(xiàn)注入到lister類中。所以,MovieLister類需要聲明一個構(gòu)造方法,它包含所需要注入的所有東西。

class MovieLister...

public MovieLister(MovieFindler findler) {
    this.findler = findler;
}

finder類本身也需要被Spring管理,text文件名也可以使用這個容器文件注入。

public class ColonDelimitedMovieFinder implements MovieFindler {
    private String fileName;

    public ColonDelimitedMovieFinder(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public List<Movie> findAll() {
        return null;
    }
}

然后,Spring需要被告知哪個接口和哪個實現(xiàn)類關(guān)聯(lián)以及需要把哪個字符串注入到finder類。換句話說,我們需要告訴容器誰需要注入,至于如何注入,由容器去完成。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="movieLister" class="com.springframework.roadmap.ioc.demo_00002.MovieLister">
        <constructor-arg ref="movieFinder"/>
    </bean>

    <bean id="movieFinder" class="com.springframework.roadmap.ioc.demo_00002.ColonDelimitedMovieFinder">
        <constructor-arg value="F:\SourceCode\SpringRoadMap\src\test\resources\movie1.txt"/>
    </bean>
</beans>
public void testConstructorInjectWithSpring() {
    ApplicationContext ctx = new FileSystemXmlApplicationContext("F:\\SourceCode\\SpringRoadMap\\src\\main\\resources" +
            "\\applicationContext.xml");
    MovieLister movieLister = (MovieLister) ctx.getBean("movieLister");
    Movie[] movies = movieLister.moviesDirectedBy("yoyo");

    System.out.println("===============" + "The Big Bang".equals(movies[0].getTitle()) + "===============");
}

我們會在另一個類中增加配置代碼,使用MovieLister類的朋友可以在配置類中編寫適合自己的配置代碼。當(dāng)然,通常會把這些配置信息以單獨的配置文件的形式保存。你可以寫一個類來讀取這個配置文件并在恰當(dāng)?shù)臅r候把這些信息配置到容器中。Spring項目的設(shè)計哲學(xué)之一就是分離配置文件與底層實現(xiàn)機制.

Setter Injection with Spring

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="movieLister" class="com.springframework.roadmap.ioc.demo_00003.MovieLister">
        <property name="movieFinder">
            <ref bean="movieFinder"/>
        </property>
    </bean>

    <bean id="movieFinder" class="com.springframework.roadmap.ioc.demo_00003.ColonDelimitedMovieFinder">
        <property name="fileName">
            <value>F:\SourceCode\SpringRoadMap\src\test\resources\movie1.txt</value>
        </property>
    </bean>
</beans>
private static void testSetterInjectWithSpring() {
    ApplicationContext ctx = new FileSystemXmlApplicationContext("F:\\SourceCode\\SpringRoadMap\\src\\test\\resources\\applicationContext_00003.xml");
    MovieLister movieLister = (MovieLister) ctx.getBean("movieLister");
    Movie[] movies = movieLister.moviesDirectedBy("yoyo");
    System.out.println("===============" + "The Big Bang".equals(movies[0].getTitle()) + "===============");

}

Interface Injection

這個不做具體展開,想要進一步了解的可以看下文末的參考資料。

對于使用者而言,無論是constructor注入還是setter注入,使用上并沒有區(qū)別,其依賴注入解析過程如下:

  • 創(chuàng)建ApplicaionContxt并根據(jù)配置的元數(shù)據(jù)(文中applicationContext_*.xml)初始化bean。配置元素數(shù)據(jù)可以使用XML、Java Code或者注解。
  • 對于每個bean,它的依賴可以使用屬性、構(gòu)造參數(shù)或者靜態(tài)工廠方法參數(shù)來表達。當(dāng)這個bean被創(chuàng)建的時候,它所依賴的bean會被自動創(chuàng)建
  • 每個屬性或構(gòu)造參數(shù)是對需要在容器中實際設(shè)置的另一個bean的值或引用的定義
  • 對于每個值類型的屬性或構(gòu)造參數(shù)可以從某種特殊的格式轉(zhuǎn)化為其實際的類型。在Spring容器中,默認將字符串格式的值轉(zhuǎn)換為內(nèi)置類型,如int、long、String、boolean等等。

Using a Service Locator

文中使用依賴注入最重要的好處就是它移除了MovieLister類對具體MovieFindler實現(xiàn)的依賴。這樣我就可以把這個MovieLister類提供給我的朋友,他們就可以在自己的環(huán)境中插入合適的實現(xiàn)(修改配置文件)。依賴注入并不是打破這種依賴的唯一方式,另外一種就是使用Service Locator(服務(wù)定位器)。

Service Locator的核心思想就是有一個對象,它知道如何獲取應(yīng)用所需要的所有服務(wù)。所以,一個應(yīng)用的Service Locator有一個方法可以返回MovieLister需要的MovieFinder對象。當(dāng)然,這只是稍微減輕了負擔(dān),我們?nèi)匀槐仨氁獙ervice Locator注入到MovieLister,所以依賴圖如下:

和注入方法一樣,我們必須配置service locator。下面直接使用代碼實現(xiàn)配置,也可以使用配置文件或注解形式。

public class ServiceLocator {
    private MovieFindler movieFindler;
    private static ServiceLocator soleInstance;

    public static MovieFindler movieFinder() {
        return soleInstance.movieFindler;
    }

    public static void load(ServiceLocator arg) {
        soleInstance = arg;
    }

    public ServiceLocator(MovieFindler movieFindler) {
        this.movieFindler = movieFindler;
    }

}
public class MovieLister {
    private MovieFindler movieFinder = ServiceLocator.movieFinder();


    public Movie[] moviesDirectedBy(String args) {
        List<Movie> allMovies = movieFinder.findAll();
        for (Iterator<Movie> it = allMovies.iterator(); it.hasNext(); ) {
            Movie movie = it.next();
            if (!(args).equals(movie.getDirector())) {
                it.remove();
            }
        }
        return allMovies.toArray(new Movie[allMovies.size()]);
    }

}
private static void configure() {
    ServiceLocator.load(new ServiceLocator(new ColonDelimitedMovieFinder("F:\\SourceCode\\SpringRoadMap" +
            "\\src\\test\\resources\\movie1.txt")));
}

private static void testServiceLocator(){
    configure();
    MovieLister movieLister = new MovieLister();

    Movie[] movies = movieLister.moviesDirectedBy("yoyo");
    System.out.println("===============" + "The Big Bang".equals(movies[0].getTitle()) + "===============");
}

如上,使用了靜態(tài)的單例對象做為ServiceLocator,從其中獲取movieFindler。顯然,這個ServiceLocator實現(xiàn)并不可以動態(tài)獲取movieFindler,就是直接返回movieFinder屬性的。并不建議這樣設(shè)計,文中只是為了方便測試。

一個更高級的或者說更加優(yōu)秀的服務(wù)定位器可以設(shè)計service locator子類并將該子類傳遞到注冊表類的屬性變量中。將ServiceLocator直接調(diào)用movieFinder屬性的方法修改為調(diào)用靜態(tài)方法,可以一個ThreadLocal變量來存儲所有的子類。這樣并不修改使用該SeviceLocator的客戶端,而且是線程相關(guān),可以根據(jù)需求使用對應(yīng)的ServiceLocator實現(xiàn)。

public class ServiceLocator {
    private MovieFindler movieFindler;
    private static ThreadLocal<ServiceLocator> threadLocal = new ThreadLocal<ServiceLocator>();

    public static MovieFindler movieFinder() {
        return getMovieFinder();
    }

    public static void load(ServiceLocator arg) {
        threadLocal.set(arg);
    }

    public ServiceLocator(MovieFindler movieFindler) {
        this.movieFindler = movieFindler;
    }

    public static MovieFindler getMovieFinder() {
        return threadLocal.get().movieFindler;

    }

}
private static void configure() {
    ServiceLocator.load(new SubServiceLocator(new ColonDelimitedMovieFinder("F:\\SourceCode\\SpringRoadMap" +
            "\\src\\test\\resources\\movie1.txt")));
}

從上面代碼可以看到,只是修改了配置configure,客戶端調(diào)用并不需要修改。而且使用ThreadLocal實現(xiàn)了類似注冊表的機制,這樣該線程后續(xù)使用的就是該注冊的Service Locator。

Using a Segregated Interface for the Locator

細心的的讀者可能會發(fā)現(xiàn),MovieLister需要依賴一個完整的服務(wù)器定位類,而你可能只需要其中一個服務(wù)。我們可以使用role interface來減少服務(wù)依賴,換句話說,MovieLister可以聲明它需要用到的服務(wù),而不用使用全部的服務(wù)定位器接口。

在這種情形下,我們可以聲明一個定位接口,它提供一個獲取finder的方法。

public interface MovieFindlerLocator {
    MovieFindler movieFindler();
}
public class ServiceLocator implements MovieFindlerLocator {
...
    @Override
    public MovieFindler movieFindler() {
        return getMovieFinder();
    }
}
public class MovieLister {
    private MovieFindler movieFinder ;
    public void setMovieFindlerLocator(MovieFindlerLocator movieFindlerLocator) {
        this.movieFinder = movieFindlerLocator.movieFindler();
    }
...

}

讀者可以看到,因為我們使用了接口,我們不可以再通過靜態(tài)方法來訪問服務(wù)。我們不惜獲取一個locator實例,然后通過它來獲取我們需要的。

A Dynamic Service Loctor

以上的例子都是靜態(tài)的,通過一個service locator提供的方法獲取我們需要的數(shù)據(jù)。顯然,還有其他方式可以用,你可以編寫一個動態(tài)的service locator,它可以允許你存儲你需要的任何服務(wù),而且你可以在運行時指定你需要哪一個。

在下面的例子中,service locator提供使用map來存儲每一個服務(wù),而不是使用一個屬性,并且提供獲取服務(wù)的通用方法。

public class ServiceLocator {
    private Map services = new ConcurrentHashMap();
    private MovieFindler movieFindler;

    private static ServiceLocator soleInstance;

    public static Object getService(String key) {
        return soleInstance.services.get(key);
    }


    public static void loadService(String key, Object value) {
        soleInstance.services.put(key, value);
    }

    public static void load(ServiceLocator locator) {
        soleInstance = locator;
    }

}


private static void configure_2() {
    ServiceLocator locator = new ServiceLocator();
    ServiceLocator.load(locator);

    ServiceLocator.loadService("MovieFindler", new ColonDelimitedMovieFinder("F:\\SourceCode\\SpringRoadMap" +
            "\\src\\test\\resources\\movie1.txt"));

}


public class MovieLister {
    private MovieFindler movieFinder = (MovieFindler) ServiceLocator.getService("MovieFindler");
...
}

總的來說,上面這個例子提供了一種獲取服務(wù)的新思路,但是需要通過一個String類型的key來獲取服務(wù),這顯然不夠標準。我更希望使用嚴格的方法定義來獲取服務(wù),因為這個很容易從接口定義中查找想要的服務(wù)。

Deciding which option to use

現(xiàn)在我已經(jīng)介紹了IOC模式及其變種,現(xiàn)在我們開始討論它們的優(yōu)缺點,進而幫助我們判斷什么時候應(yīng)該使用哪種實現(xiàn)。

Service Locator vs Dependency Injection

兩者都提供了解耦功能,面向接口開發(fā),具體實現(xiàn)與接口分離。兩者主要的不同在于,提供給應(yīng)用的使用方式。使用Service Locator時,應(yīng)用需要顯式的通過locator定位服務(wù),而DI并不需要顯式的請求,而是利用容器來實現(xiàn)服務(wù)注入進而實現(xiàn)了控制反轉(zhuǎn)。

IOC是框架的一個通用特性,但是在獲取IOC有點的同時也需要付出對應(yīng)的代價。尤其是當(dāng)你debug時很難理解并且找到問題產(chǎn)生的原因。所以,我們在使用時要根據(jù)自身需求,審慎的選擇。

兩者之間的一個關(guān)鍵不同是每一個應(yīng)用都對Service Locator有依賴性,但是Service Locator隱藏了對其他實現(xiàn)的依賴。所以兩者之間的選擇取決于依賴是不是一個問題。

使用DI可以幫助我們更容易看到組件的依賴是什么,現(xiàn)在的IDE可以很容易查找DI的依賴是什么并找到這些依賴。但是,如果應(yīng)用需要的東西都可以通過一個或幾個服務(wù)來完成,那么依賴就不是問題,DI的也就沒什么優(yōu)勢了。

因為有了DI容器,你可以不必處理組件到容器的依賴關(guān)系,但是一旦配置好容器,組件就不能從容器中獲取進一步的服務(wù)。

Constructor versus Setter Injection

當(dāng)我們需要組合使用一些服務(wù)的時候,為了把他們組裝到一起,我們需要制定一些規(guī)則。DI的優(yōu)勢在于它的規(guī)則很簡單,比如coustructor和setter注入,你不必去讓組件做一些古怪的事情而且它的配置很簡單。與之相對的接口注入,則并不贊成使用,使用這種方式的注入,需要制定過多的接口,且不易維護。

constructor和setter注入反映出面向?qū)ο缶幊痰囊粋€普遍問題,你需要通過setter或constructor填充屬性。使用constructor構(gòu)造注入可以在一開始就制定需要使用的屬性,封裝性更好。相對而言,setter過多則顯得冗余,但是參數(shù)過多的構(gòu)造函數(shù)注入也是很恐怖的,這個時候就需要把對象進行拆分了。

構(gòu)造函數(shù)注入依賴參數(shù)的未知,如果都是相同類型,則易出錯,在這一方面,setter注入就沒有這個困擾,注入更加具有指向性。所以,現(xiàn)在的大部分框架兩種注入都支持的,比如Spring,根據(jù)需求選擇合適的注入方式即可。

自動注入VS手動注入

Code or configuration files

有一個常見的問題:1.使用配置文件來裝配服務(wù) 2.使用代碼API裝配服務(wù)。因為大部分應(yīng)用可能被發(fā)布到不同場景,一個分離的配置文件還是很有意義的。很多時候這可能是xml文件,但是有些場景下直接使用代碼來裝配更容易。當(dāng)我們的應(yīng)用比較簡單而且發(fā)布場景也比較單一的情況下,使用代碼比XML文件更清晰自然。

尤其在裝配時需要根據(jù)條件裝配不同的對象時,使用代碼比使用XML文件指定依賴更容易且更易維護。當(dāng)然也可以使用一個簡單的XML配置文件,然后編寫創(chuàng)建類,根據(jù)配置文件來選擇如何裝配。

很多時候,人們總是急于定義配置文件。其實,一般來說,編程語言可以制定簡單而強大的配置機制?,F(xiàn)代編程語言可以容易的編寫裝配器來把插件裝配到大型系統(tǒng),如果編譯是個問題,我們還可以使用腳本語言,如python等。

有一個不和諧的問題,那就是每個組件維護一套自己的配置文件,而使用者需要保持這些配置文件的同步正確,這很讓人困擾。

我的建議是使用編程接口來進行配置,另外,再提供一個配置文件來關(guān)聯(lián)編程接口,這樣我們就可以進行簡單的配置就可以實現(xiàn)功能。在設(shè)計層面,SpringBoot簡化依賴及構(gòu)建過程的思想及去配置化的思路有異曲同工之妙。

Separating Configuration from Use

配置與業(yè)務(wù)邏輯分離這與面向接口開發(fā)的OO思想殊途同歸,這樣更易解耦與擴展。使用多態(tài)而不是條件判斷來決定實例化哪一個對象。

配置機制可以用來配置Service Locator或使用DI方式來直接配置對象。配置機制可以讓應(yīng)用適配不同的組件,就像一個適配器或插件,讓我們的應(yīng)用更易組合不同組件并適配不同的環(huán)境。

Concluding Thoughts

當(dāng)下的輕量級容器底層的原理多是有一個共性:如何裝配服務(wù)/組件(即依賴注入模式)。

相對而言,Service Locator指向性更加明確,但是如果開發(fā)的組件是供不同應(yīng)用使用的情況下,DI則更合適。

最重要的是將服務(wù)/組件的配置與服務(wù)/組件的使用分離的原則及思想,這比在DI和Service Locator之間選擇更有價值。

Spring 部分依賴圖

image

PS:
Spring處理bean步驟:

  • 分離出配置文件
  • 讀取配置文件
  • 根據(jù)配置文件生成對象工廠
  • 使用對象時,從工廠中?。ㄗ⒔饣騒ML聲明實現(xiàn))而無需自己去new
  • 對象都是由容器在啟動時或使用時給new好了,直接聲明使用(注解或XML文件配置),無需自己new
    Spring核心思想:
  • 對于一個Java Bean來說,如果你依賴別的Bean ,只需要聲明(注解或XML配置)即可,spring 容器負責(zé)把依賴的bean 給“注入進去“, 這就是控制反轉(zhuǎn)(IoC)
  • 如果一個Java Bean需要一些像事務(wù),日志,安全這樣的通用的服務(wù),也是只需要聲明(注解或XML配置)即可, spring 容器在運行時能夠動態(tài)的“織入”這些服務(wù), 這就是AOP

參考書籍:
Expert One-on-One J2EE Design and Development
Expert one on one J2EE development withoutEJB

參考資料

https://martinfowler.com/articles/injection.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,844評論 2 372

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