SpringIOC(控制反轉(zhuǎn))與DI(依賴注入)

好了,我看過了寫的最好的了,講的很詳細

Spring IoC有什么好處呢? - 知乎

但不以為著此貼終結(jié)啊。我覺得我這篇講的很詳細,雖然不如知乎上的通俗易懂,但是更顯專業(yè)orz。

2019年11月7日更新



? ? ? 相信大家和我一樣初學(xué)springIOC和DI時都有些迷糊吧,反正我是看了會資料了解了大概就繼續(xù)看其他的內(nèi)容了,想著多學(xué)點知識,好讀書,不求甚解。以為多學(xué)點就能處理問題了。后來上班實習(xí)發(fā)現(xiàn)這些知識挺重要的,想著有時間看資料總結(jié)一下,為了自己更好的理解,也幫助大家,因為一是有些忙,二是想寫好點。就是可能多次更新,寫一寫。+

分享一下牛人對于IOC的理解。地址:http://jinnianshilongnian.iteye.com/blog/1413846

以下是原文:

? ? ? ? Ioc—Inversion of Control,即“控制反轉(zhuǎn)”,不是什么技術(shù),而是一種設(shè)計思想。在Java開發(fā)中,Ioc意味著將你設(shè)計好的對象交給容器控制,而不是傳統(tǒng)的在你的對象內(nèi)部直接控制。如何理解好Ioc呢?理解好Ioc的關(guān)鍵是要明確“誰控制誰,控制什么,為何是反轉(zhuǎn)(有反轉(zhuǎn)就應(yīng)該有正轉(zhuǎn)了),哪些方面反轉(zhuǎn)了”,那我們來深入分析一下:

●誰控制誰,控制什么:傳統(tǒng)Java SE程序設(shè)計,我們直接在對象內(nèi)部通過new進行創(chuàng)建對象,是程序主動去創(chuàng)建依賴對象;而IoC是有專門一個容器來創(chuàng)建這些對象,即由Ioc容器來控制對象的創(chuàng)建;誰控制誰?當然是IoC 容器控制了對象;控制什么?那就是主要控制了外部資源獲?。ú恢皇菍ο蟀ū热缥募龋?/p>

為何是反轉(zhuǎn),哪些方面反轉(zhuǎn)了:有反轉(zhuǎn)就有正轉(zhuǎn),傳統(tǒng)應(yīng)用程序是由我們自己在對象中主動控制去直接獲取依賴對象,也就是正轉(zhuǎn);而反轉(zhuǎn)則是由容器來幫忙創(chuàng)建及注入依賴對象;為何是反轉(zhuǎn)?因為由容器幫我們查找及注入依賴對象,對象只是被動的接受依賴對象,所以是反轉(zhuǎn);哪些方面反轉(zhuǎn)了?依賴對象的獲取被反轉(zhuǎn)了。

用圖例說明一下,傳統(tǒng)程序設(shè)計如圖,都是主動去創(chuàng)建相關(guān)對象然后再組合起來:


傳統(tǒng)應(yīng)用程序示意圖

當有了IoC/DI的容器后,在客戶端類中不再主動去創(chuàng)建這些對象了,如圖所示


有IoC/DI容器后程序結(jié)構(gòu)示意圖

1.1.2? IoC能做什么

IoC不是一種技術(shù),只是一種思想,一個重要的面向?qū)ο缶幊痰姆▌t,它能指導(dǎo)我們?nèi)绾卧O(shè)計出松耦合、更優(yōu)良的程序。傳統(tǒng)應(yīng)用程序都是由我們在類內(nèi)部主動創(chuàng)建依賴對象,從而導(dǎo)致類與類之間高耦合,難于測試;有了IoC容器后,把創(chuàng)建和查找依賴對象的控制權(quán)交給了容器,由容器進行注入組合對象,所以對象與對象之間是松散耦合,這樣也方便測試,利于功能復(fù)用,更重要的是使得程序的整個體系結(jié)構(gòu)變得非常靈活。

其實IoC對編程帶來的最大改變不是從代碼上,而是從思想上,發(fā)生了“主從換位”的變化。應(yīng)用程序原本是老大,要獲取什么資源都是主動出擊,但是在IoC/DI思想中,應(yīng)用程序就變成被動的了,被動的等待IoC容器來創(chuàng)建并注入它所需要的資源了。

IoC很好的體現(xiàn)了面向?qū)ο笤O(shè)計法則之一—— 好萊塢法則:“別找我們,我們找你”;即由IoC容器幫對象找相應(yīng)的依賴對象并注入,而不是由對象主動去找。

2.1.3? IoC和DI

? ? ? DI—Dependency Injection,即“依賴注入”:是組件之間依賴關(guān)系由容器在運行期決定,形象的說,即由容器動態(tài)的將某個依賴關(guān)系注入到組件之中。依賴注入的目的并非為軟件系統(tǒng)帶來更多功能,而是為了提升組件重用的頻率,并為系統(tǒng)搭建一個靈活、可擴展的平臺。通過依賴注入機制,我們只需要通過簡單的配置,而無需任何代碼就可指定目標需要的資源,完成自身的業(yè)務(wù)邏輯,而不需要關(guān)心具體的資源來自何處,由誰實現(xiàn)。

理解DI的關(guān)鍵是:“誰依賴誰,為什么需要依賴,誰注入誰,注入了什么”,那我們來深入分析一下:

誰依賴于誰:當然是應(yīng)用程序依賴于IoC容器;

為什么需要依賴:應(yīng)用程序需要IoC容器來提供對象需要的外部資源;

誰注入誰:很明顯是IoC容器注入應(yīng)用程序某個對象,應(yīng)用程序依賴的對象;

●注入了什么:就是注入某個對象所需要的外部資源(包括對象、資源、常量數(shù)據(jù))。

IoC和DI由什么關(guān)系呢?其實它們是同一個概念的不同角度描述,由于控制反轉(zhuǎn)概念比較含糊(可能只是理解為容器控制對象這一個層面,很難讓人想到誰來維護對象關(guān)系),所以2004年大師級人物Martin Fowler又給出了一個新的名字:“依賴注入”,相對IoC 而言,依賴注入”明確描述了“被注入對象依賴IoC容器配置依賴對象”。

注:如果想要更加深入的了解IoC和DI,請參考大師級人物Martin Fowler的一篇經(jīng)典文章《Inversion of Control Containers and the Dependency Injection pattern》,原文地址:http://www.martinfowler.com/articles/injection.html。

?然后我就去讀了這篇文章,把他大致總結(jié)一下:

Components and Services(組件和服務(wù))

現(xiàn)有許多資料談組件和服務(wù)都很繁瑣和晦澀,我們應(yīng)該用盡量清楚明白的語言來闡述。

I use component to mean a glob of software that's intended to be used, without change, by an application that is out of the control of the writers of the component. ----->組件應(yīng)該是不變的。By 'without change' I mean that the using application doesn't change the source code of the components, although they may alter the component's behavior by extending it in ways allowed by the component writers.--->進一步闡述了組件為什么不變。趨向于擴展而不是改變它本身的源代碼。

A service is similar to a component in that it's used by foreign applications.----->服務(wù)類似于組件,被外部應(yīng)用程序所使用。The main difference is that I expect a component to be used locally (think jar file, assembly, dll, or a source import). A service will be used remotely through some remote interface, either synchronous or asynchronous (eg web service, messaging system, RPC, or socket.)

I mostly use service in this article, but much of the same logic can be applied to local components too. Indeed often you need some kind of local component framework to easily access a remote service. But writing "component or service" is tiring to read and write, and services are much more fashionable at the moment.------>我在本文中主要使用service,但是同樣的邏輯也可以應(yīng)用于本地組件。實際上,您經(jīng)常需要某種本地組件框架來方便地訪問遠程服務(wù)。但是編寫“組件或服務(wù)”的讀寫是很累人的,而且目前使用“服務(wù)“”要流行得多。

A Naive Example

接下來作者舉了一個例子。

在本例中,我正在編寫一個組件,它提供由特定導(dǎo)演執(zhí)導(dǎo)的電影列表。這個非常有用的函數(shù)是由一個方法實現(xiàn)的。

? 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()]);

? }

? ? ? ?這個函數(shù)的實現(xiàn)非常簡單,它要求finder對象(我們稍后會講到)返回它知道的所有影片。然后它只是搜索這個列表,返回由特定導(dǎo)演執(zhí)導(dǎo)的影片。本文真正的重點是這個finder對象,特別是如何將lister對象與特定的finder對象連接起來。這很有趣的原因是,我想讓我的美妙的影片通過方法來導(dǎo)演,完全獨立于所有影片的存儲方式。這個方法所做的就是引用finder,而這個finder所做的就是知道如何響應(yīng)findAll方法。我可以通過為finder定義一個接口來實現(xiàn)這一點。

public interface MovieFinder {

? ? List findAll();

}

現(xiàn)在,所有這些都很好地解耦了,但在某個時候,我必須想出一個具體的類來處理電影。在本例中,我將其代碼放入lister類的構(gòu)造函數(shù)中。

class MovieLister...

? private MovieFinder finder;

? public MovieLister() {

? ? finder = new ColonDelimitedMovieFinder("movies1.txt");

? }

The name of the implementation class comes from the fact that I'm getting my list from a colon delimited text file.用冒號分隔文件,現(xiàn)在,如果我只是為自己使用這個類,這一切都很好。但是,當我的朋友們被這種美妙的功能所吸引,想要復(fù)制我的程序時,會發(fā)生什么呢?如果他們還將電影列表存儲在一個冒號分隔的文本文件中,該文本文件名為“movies1”.txt“那么一切都是美好的。如果它們的電影文件有不同的名稱,那么很容易將文件的名稱放入屬性文件中。但是,如果他們有完全不同的存儲電影列表的形式:SQL數(shù)據(jù)庫、XML文件、web服務(wù),或者只是另一種文本文件格式呢?在本例中,我們需要一個不同的類來獲取數(shù)據(jù)?,F(xiàn)在因為我已經(jīng)定義了一個MovieFinder接口,這不會改變我的moviesDirectedBy方法。但是,我仍然需要一些方法來獲得right finder實現(xiàn)的實例。


關(guān)系圖

? ? ? ?上圖顯示了這種情況的依賴關(guān)系。MovieLister類依賴于MovieFinder接口和實現(xiàn)。如果它只依賴于接口,我們會更喜歡它,但是我們?nèi)绾蝿?chuàng)建一個實例來使用它。

? ? ? ? ? ?在我的書《?P of EAA》(Patterns of Enterprise Application Architecture)中,我們將這種情況描述為插件。finder的實現(xiàn)類在編譯時沒有鏈接到程序中,因為我不知道我的朋友將使用什么。相反,我們希望我的lister能夠與任何實現(xiàn)一起工作,并在稍后的某個時候插入該實現(xiàn),而不受我的控制。問題是,我如何創(chuàng)建這個鏈接,使lister類不知道實現(xiàn)類,但仍然可以與實例對話來完成它的工作。

? ? ? ? ? ?將其擴展到一個真實的系統(tǒng)中,我們可能有幾十個這樣的服務(wù)和組件。在每種情況下,我們都可以通過接口與這些組件進行通信來抽象我們對它們的使用(如果組件的設(shè)計沒有考慮到接口,則使用適配器)。但是如果我們希望以不同的方式部署這個系統(tǒng),我們需要使用插件來處理與這些服務(wù)的交互,這樣我們就可以在不同的部署中使用不同的實現(xiàn)。

? ? ? ? ? ?因此,核心問題是如何將這些插件組裝到應(yīng)用程序中?這是這種新型輕量級容器所面臨的主要問題之一,而且它們通常都使用反轉(zhuǎn)控制來實現(xiàn)這一點。

Inversion of Control

? ? ? 當這些容器談到它們是如何如此有用,因為它們實現(xiàn)了“控制反轉(zhuǎn)”時,我感到非常困惑。反轉(zhuǎn)控制是框架的一個常見特性,所以說這些輕量級容器是特殊的,因為它們使用反轉(zhuǎn)控制,就像說我的車是特殊的,因為它有輪子一樣。問題是:“他們改變了控制的哪些方面?”當我第一次遇到控制反轉(zhuǎn)時,它是在一個用戶界面的主控制中。早期的用戶界面由應(yīng)用程序控制。你會有一系列的命令,比如“輸入名字”,“輸入地址”;您的程序?qū)Ⅱ?qū)動提示并對每個提示進行響應(yīng)。對于圖形化(甚至基于屏幕的)UI, UI框架將包含這個主循環(huán),而您的程序?qū)槠聊簧系母鱾€字段提供事件處理程序。程序的主控件被倒置,從您移到框架上。

? ? ? ?對于這種新類型的容器,反轉(zhuǎn)是關(guān)于它們?nèi)绾尾檎也寮崿F(xiàn)的。在我的簡單示例中,lister通過直接實例化查找finder實現(xiàn)。這將阻止finder成為插件。這些容器使用的方法是確保插件的任何用戶都遵循某種約定,允許一個單獨的組裝模塊將實現(xiàn)注入lister。因此,我認為我們需要為這種模式取一個更具體的名稱??刂品崔D(zhuǎn)這個術(shù)語太籠統(tǒng)了,因此人們會感到困惑。因此,在與各種IoC支持者進行了大量討論之后,我們確定了名稱依賴注入。我將首先討論各種形式的依賴注入,但現(xiàn)在我要指出,這不是將依賴項從應(yīng)用程序類移到插件實現(xiàn)的唯一方法。您可以使用的另一種模式是Service Locator,我將在解釋完依賴項注入之后討論它。(個人觀點:此點體現(xiàn)了低耦合,使得其擴展性更強)

Forms of Dependency Injection

依賴項注入的基本思想是擁有一個單獨的對象(一個匯編程序),該對象使用finder接口的適當實現(xiàn)填充lister類中的字段,生成如下圖所示的依賴關(guān)系圖


關(guān)系圖2

? ? ? ?依賴項注入有三種主要類型。我為它們使用的名稱是構(gòu)造函數(shù)注入、Setter注入和接口注入。如果您在當前關(guān)于控制反轉(zhuǎn)的討論中讀到過這方面的內(nèi)容,那么您將聽到這些被稱為類型1 IoC(接口注入)、類型2 IoC (setter注入)和類型3 IoC(構(gòu)造函數(shù)注入)。我發(fā)現(xiàn)數(shù)字名稱很難記住,這就是為什么我使用這里的名稱。

Constructor Injection with PicoContainer

? ? ? 首先,我將展示如何使用一個名為PicoContainer的輕量級容器來完成這個注入。我之所以從這里開始,主要是因為我在ThoughtWorks的幾位同事對PicoContainer的開發(fā)非常積極(是的,這是一種企業(yè)裙帶關(guān)系)。PicoContainer使用構(gòu)造函數(shù)來決定如何將查找器實現(xiàn)注入lister類。要實現(xiàn)這一點,movie lister類需要聲明一個構(gòu)造函數(shù),其中包含它需要注入的所有內(nèi)容。

public MovieLister(MovieFinder finder) {

? ? ? this.finder = finder;? ? ?

? }

finder本身也將由pico容器管理,因此將文本文件的文件名由容器注入其中。

class ColonMovieFinder...

? public ColonMovieFinder(String filename) {

? ? ? this.filename = filename;

? }

然后需要告訴pico容器要與每個接口關(guān)聯(lián)的實現(xiàn)類,以及要注入到finder中的字符串。

private MutablePicoContainer configureContainer() {

? ? MutablePicoContainer pico = new DefaultPicoContainer();

? ? Parameter[] finderParams =? {new ConstantParameter("movies1.txt")};

? ? pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);

? ? pico.registerComponentImplementation(MovieLister.class);

? ? return pico;

}

這個配置代碼通常在另一個類中設(shè)置。對于我們的示例,每個使用我的lister的朋友都可以在他們自己的setup類中編寫適當?shù)呐渲么a。當然,將這類配置信息保存在單獨的配置文件中是很常見的。您可以編寫一個類來讀取配置文件并適當?shù)卦O(shè)置容器。盡管PicoContainer本身不包含此功能,但是有一個密切相關(guān)的項目NanoContainer,它提供了適當?shù)陌b器,允許您擁有XML配置文件。這樣的nano容器將解析XML,然后配置底層的pico容器。該項目的理念是將配置文件格式與底層機制分離。要使用容器,可以編寫類似這樣的代碼。

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());

}

雖然在這個例子中我使用了構(gòu)造函數(shù)注入,但是PicoContainer也支持setter注入,盡管它的開發(fā)人員更喜歡構(gòu)造函數(shù)注入。

Setter Injection with Spring

Spring框架是一個用于企業(yè)Java開發(fā)的廣泛框架。它包括事務(wù)抽象層、持久性框架、web應(yīng)用程序開發(fā)和JDBC。與PicoContainer一樣,它支持構(gòu)造函數(shù)注入和setter注入,但是它的開發(fā)人員傾向于選擇setter注入——這使得它成為本例中的一個合適選擇。為了讓我的movie lister接受注入,我為該服務(wù)定義了一個設(shè)置方法

class MovieLister...

? private MovieFinder finder;

public void setFinder(MovieFinder finder) {

? this.finder = finder;

}

Similarly I define a setter for the filename.定義了一個setter

class ColonMovieFinder...

? public void setFilename(String filename) {

? ? ? this.filename = filename;

? }

第三步是設(shè)置文件的配置。Spring支持通過XML文件和代碼進行配置,但XML是要按照預(yù)期的方式去做。如下

<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 = new FileSystemXmlApplicationContext("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());

}

Interface Injection

第三種注入技術(shù)是為注入定義和使用接口。Avalon就是這樣在某些地方使用這種技術(shù)的框架的一個例子。稍后我會詳細討論這一點,但在本例中,我將使用一些簡單的示例代碼。使用這種技術(shù),我首先定義一個接口,我將使用它來執(zhí)行注入。下面是將影片查找器(finder)注入對象的接口。

public interface InjectFinder {

? ? void injectFinder(MovieFinder finder);

}

這個接口將由提供MovieFinder接口的人定義。它需要由希望使用finder(如lister)的任何類實現(xiàn)。

class MovieLister implements InjectFinder

? public void injectFinder(MovieFinder finder) {

? ? ? this.finder = finder;

? }

我使用類似的方法將文件名注入finder實現(xiàn)。

public interface InjectFinderFilename {

? ? void injectFilename (String filename);

}

class ColonMovieFinder implements MovieFinder, InjectFinderFilename...

? public void injectFilename(String filename) {

? ? ? this.filename = filename;

? }

然后,像往常一樣,我需要一些配置代碼來連接實現(xiàn)。為了簡單起見,我將用代碼來做。

class Tester...

? private Container container;

? private void configureContainer() {

? ? container = new Container();

? ? registerComponents();

? ? registerInjectors();

? ? container.start();

? }

這個配置有兩個階段,通過查找鍵注冊組件與其他示例非常相似。

class Tester...

? private void registerComponents() {

? ? container.registerComponent("MovieLister", MovieLister.class);

? ? container.registerComponent("MovieFinder", ColonMovieFinder.class);

? }

一個新的步驟是注冊將注入相關(guān)組件的注入器。每個注入接口都需要一些代碼來注入依賴對象。在這里,我通過向容器注冊注入器對象來實現(xiàn)這一點。每個注入器對象實現(xiàn)注入器接口。

class Tester...

? private void registerInjectors() {

? ? container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));

? ? container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());

? }

public interface Injector {

? public void inject(Object target);

}

當依賴項是為這個容器編寫的類時,組件本身實現(xiàn)注入器接口是有意義的,就像我在這里使用movie finder所做的一樣。對于泛型類,例如字符串,我在配置代碼中使用一個內(nèi)部類。

class ColonMovieFinder implements Injector...

? public void inject(Object target) {

? ? ((InjectFinder) target).injectFinder(this);? ? ? ?

? }

class Tester...

? public static class FinderFilenameInjector implements Injector {

? ? public void inject(Object target) {

? ? ? ((InjectFinderFilename)target).injectFilename("movies1.txt");? ? ?

? ? }

? ? }

然后測試使用容器。

class Tester…

? public void testIface() {

? ? configureContainer();

? ? MovieLister lister = (MovieLister)container.lookup("MovieLister");

? ? Movie[] movies = lister.moviesDirectedBy("Sergio Leone");

? ? assertEquals("Once Upon a Time in the West", movies[0].getTitle());

? }

容器使用聲明的注入接口來確定依賴項,并使用注入器注入正確的依賴項。(我在這里所做的特定容器實現(xiàn)對該技術(shù)并不重要,我不會展示它,因為您只會一笑置之。)

Using a Service Locator

依賴注入器的主要好處是,它消除了MovieLister類對具體MovieFinder實現(xiàn)的依賴。這允許我將listers提供給朋友,并讓他們插入適合自己環(huán)境的實現(xiàn)。注入不是打破這種依賴關(guān)系的唯一方法,另一種方法是使用服務(wù)定位器。服務(wù)定位器背后的基本思想是擁有一個知道如何獲取應(yīng)用程序可能需要的所有服務(wù)的對象。因此,此應(yīng)用程序的服務(wù)定位器將具有一個方法,該方法在需要時返回影片查找器。當然,這只是稍微轉(zhuǎn)移了一些負擔,我們?nèi)匀恍枰獙⒍ㄎ黄鞣湃雔ister中,這導(dǎo)致了下圖中的依賴關(guān)系


關(guān)系圖

在本例中,我將使用ServiceLocator作為單例注冊表。lister然后可以在實例化finder時使用它來獲取finder。

class MovieLister...

? MovieFinder finder = ServiceLocator.movieFinder();

class ServiceLocator...

? public static MovieFinder movieFinder() {

? ? ? return soleInstance.movieFinder;

? }

? private static ServiceLocator soleInstance;

? private MovieFinder movieFinder;

與注入方法一樣,我們必須配置服務(wù)定位器。這里我是用代碼來做的,但是使用從配置文件中讀取適當數(shù)據(jù)的機制并不難。

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。?!?/p>

由于篇幅實在太多,后面設(shè)計服務(wù)定位器(Service Locator)等知識,可以自行去查看。

? ? 到目前為止,我一直專注于解釋我如何看待這些模式及其變化?,F(xiàn)在,我可以開始討論它們的優(yōu)缺點,以幫助確定使用哪些方法以及何時使用。

Service Locator vs Dependency Injection

基本的選擇是服務(wù)定位器和依賴項注入之間的選擇。第一點是,這兩種實現(xiàn)都提供了基本的解耦,而這正是樸素示例中所缺少的——在這兩種情況下,應(yīng)用程序代碼都獨立于服務(wù)接口的具體實現(xiàn)。這兩種模式之間的重要區(qū)別在于如何將實現(xiàn)提供給應(yīng)用程序類。使用服務(wù)定位器,應(yīng)用程序類通過發(fā)送給定位器的消息顯式地請求它。注入沒有顯式的請求,服務(wù)出現(xiàn)在應(yīng)用程序類中——因此控制反轉(zhuǎn)。控制反轉(zhuǎn)是框架的一個常見特性,但它是有代價的。當您嘗試調(diào)試時,它往往很難理解并導(dǎo)致問題。所以總的來說,我寧愿避免它,除非我需要它。這并不是說這是件壞事,只是我認為它需要證明自己,而不是更直接的選擇。

關(guān)鍵的區(qū)別在于,使用服務(wù)定位器時,服務(wù)的每個用戶都對定位器具有依賴關(guān)系。定位器可以隱藏對其他實現(xiàn)的依賴關(guān)系,但您確實需要查看定位器。所以定位器和注入器之間的決定取決于依賴關(guān)系是否存在問題。使用依賴項注入可以幫助更容易地查看組件依賴項是什么。使用依賴注入器,您可以只查看注入機制,比如構(gòu)造函數(shù),然后查看依賴項。使用服務(wù)定位器,您必須搜索對定位器的調(diào)用的源代碼。帶有find references功能的現(xiàn)代ide使這一點變得更容易,但它仍然不像查看構(gòu)造函數(shù)或設(shè)置方法那么容易。

這在很大程度上取決于服務(wù)用戶的性質(zhì)。如果您正在使用使用服務(wù)的各種類構(gòu)建應(yīng)用程序,那么應(yīng)用程序類與定位器之間的依賴關(guān)系不是什么大問題。在我為朋友提供電影列表的例子中,使用服務(wù)定位器工作得非常好。他們所需要做的就是配置定位器,以便通過一些配置代碼或配置文件將正確的服務(wù)實現(xiàn)掛鉤起來。在這種情況下,我不認為注入器的反轉(zhuǎn)提供了任何令人信服的東西。如果lister是我提供給其他人正在編寫的應(yīng)用程序的組件,就會有不同。在這種情況下,我不太了解我的客戶將要使用的服務(wù)定位器的api。每個客戶可能都有自己不兼容的服務(wù)定位器。我可以通過使用隔離接口來解決一些問題。每個客戶都可以編寫一個適配器,使我的接口與他們的定位器匹配,但是在任何情況下,我仍然需要看到第一個定位器來查找我的特定接口。一旦適配器出現(xiàn),直接連接到定位器的簡單性就開始下滑。

由于使用注入器時,組件與注入器之間不存在依賴關(guān)系,因此一旦配置好注入器,組件就無法從注入器獲得進一步的服務(wù)。人們更喜歡依賴注入的一個常見原因是,它使測試更容易。這里的重點是,要進行測試,您需要輕松地用存根或模擬替換真正的服務(wù)實現(xiàn)。然而,依賴項注入和服務(wù)定位器之間實際上沒有區(qū)別:兩者都非常適合存根。我懷疑這種觀察來自于人們沒有努力確保他們的服務(wù)定位器可以很容易地替換的項目。這就是持續(xù)測試的作用所在,如果您不能輕松地為測試存根服務(wù),那么這就意味著您的設(shè)計存在嚴重的問題。

當然,測試問題由于組件環(huán)境(如Java的EJB框架)非常具有侵入性而加劇。我的觀點是,這類框架應(yīng)該盡量減少它們對應(yīng)用程序代碼的影響,特別是不應(yīng)該做一些降低編輯執(zhí)行周期的事情。使用插件來替代重量級組件對這個過程有很大幫助,這對于測試驅(qū)動開發(fā)之類的實踐非常重要。所以,主要的問題是,對于那些編寫代碼的人來說,他們希望在作者控制之外的應(yīng)用程序中使用這些代碼。在這些情況下,即使是關(guān)于服務(wù)定位器的最小假設(shè)也是一個問題。

Constructor versus Setter Injection

對于服務(wù)組合,您總是必須有某種約定才能將內(nèi)容連接在一起。注入的優(yōu)勢主要在于它需要非常簡單的約定——至少對于構(gòu)造函數(shù)和setter注入是這樣。您不需要在組件中執(zhí)行任何奇怪的操作,而且對于注入器來說,配置一切都非常簡單。接口注入更具侵入性,因為您必須編寫大量接口才能把所有事情都處理好。對于容器所需的一小組接口,比如Avalon的方法,這并不太糟糕。但是組裝組件和依賴項需要大量的工作,這就是為什么當前的輕量級容器使用setter和構(gòu)造函數(shù)注入。

? ? ? setter和構(gòu)造函數(shù)注入之間的選擇很有趣,因為它反映了面向?qū)ο缶幊痰囊粋€更普遍的問題——應(yīng)該在構(gòu)造函數(shù)中填充字段還是用setter填充字段。我長期運行的默認對象是盡可能多地在構(gòu)建時創(chuàng)建有效的對象。這個建議可追溯到Kent Beck的《Smalltalk最佳實踐模式:構(gòu)造函數(shù)方法和構(gòu)造函數(shù)參數(shù)方法》。帶有參數(shù)的構(gòu)造函數(shù)可以清楚地說明在一個明顯的位置創(chuàng)建一個有效對象意味著什么。如果有不止一種方法,創(chuàng)建多個顯示不同組合的構(gòu)造函數(shù)。

? ? ? ?構(gòu)造函數(shù)初始化的另一個優(yōu)點是,通過不提供setter,可以清楚地隱藏任何不可變的字段。我認為這很重要——如果某些東西不應(yīng)該改變,那么缺少setter可以很好地傳達這一點。如果您使用setter進行初始化,那么這會成為一種痛苦。(事實上,在這些情況下,我更愿意避免通常的設(shè)置約定,我更愿意使用initFoo這樣的方法,來強調(diào)這只是在出生時才應(yīng)該做的事情。)但任何情況都有例外。如果有很多構(gòu)造函數(shù)參數(shù),事情就會變得很混亂,特別是在沒有關(guān)鍵字參數(shù)的語言中。確實,長構(gòu)造函數(shù)通常是應(yīng)該分割的過于繁忙的對象的標志,但是在某些情況下,這正是您所需要的。

? ? ?如果有多種構(gòu)造有效對象的方法,則很難通過構(gòu)造函數(shù)來展示這一點,因為構(gòu)造函數(shù)只能根據(jù)參數(shù)的數(shù)量和類型而變化。這時工廠方法開始發(fā)揮作用,它們可以使用私有構(gòu)造函數(shù)和setter的組合來實現(xiàn)它們的工作。用于組件組裝的經(jīng)典工廠方法的問題是,它們通常被視為靜態(tài)方法,而不能在接口上使用這些方法。您可以創(chuàng)建一個工廠類,但是這樣就變成了另一個服務(wù)實例。工廠服務(wù)通常是一種很好的策略,但是您仍然必須使用其中一種技術(shù)實例化工廠。如果有簡單的參數(shù),比如字符串,構(gòu)造函數(shù)也會受到影響。通過setter注入,您可以為每個setter指定一個名稱,以指示字符串應(yīng)該做什么。對于構(gòu)造函數(shù),您只是依賴于位置,而位置更難跟蹤

? ? ?如果您有多個構(gòu)造函數(shù)和繼承,那么事情就會變得特別棘手。為了初始化所有內(nèi)容,您必須提供構(gòu)造函數(shù)來轉(zhuǎn)發(fā)給每個超類構(gòu)造函數(shù),同時還要添加自己的參數(shù)。這可能導(dǎo)致構(gòu)造函數(shù)的更大爆炸。盡管有這些缺點,但我更傾向于從構(gòu)造函數(shù)注入開始,但是一旦上面列出的問題開始成為問題,就要準備切換到setter注入。這個問題在提供依賴注入器作為框架一部分的各個團隊之間引發(fā)了很多爭論。然而,構(gòu)建這些框架的大多數(shù)人似乎已經(jīng)意識到支持這兩種機制很重要,即使偏愛其中一種。

Code or configuration files

? ? ?另一個經(jīng)常合并的問題是,是否使用配置文件或API上的代碼來連接服務(wù)。對于大多數(shù)可能部署在許多地方的應(yīng)用程序,單獨的配置文件通常最有意義。幾乎所有時候這都是一個XML文件,這是有意義的。然而,在某些情況下,使用程序代碼進行組裝更容易。一種情況是,您有一個簡單的應(yīng)用程序,它沒有太多的部署變化。在這種情況下,一些代碼比單獨的XML文件更清晰。相反的情況是,程序集非常復(fù)雜,涉及條件步驟。一旦您開始接近編程語言,那么XML就開始分解,最好使用一種真正的語言,它具有編寫清晰程序所需的所有語法。然后編寫一個執(zhí)行組裝的構(gòu)建器類。如果您有不同的構(gòu)建器場景,您可以提供幾個構(gòu)建器類,并使用一個簡單的配置文件在它們之間進行選擇。我經(jīng)常認為人們過于急于定義配置文件。通常,編程語言會創(chuàng)建一個簡單而強大的配置機制?,F(xiàn)代語言可以很容易地編譯小型匯編程序,這些程序可用于為大型系統(tǒng)組裝插件。如果編譯是一個痛苦的過程,那么有些腳本語言也可以很好地工作。

經(jīng)常有人說,配置文件不應(yīng)該使用編程語言,因為它們需要由非程序員編輯。但這種情況多久發(fā)生一次呢?人們真的希望非程序員更改復(fù)雜服務(wù)器端應(yīng)用程序的事務(wù)隔離級別嗎?非語言配置文件只有在簡單的情況下才能很好地工作。如果它們變得復(fù)雜,那么是時候考慮使用合適的編程語言了。目前我們在Java世界中看到的一件事是配置文件的不和諧,其中每個組件都有自己的配置文件,這些文件與其他組件的配置文件不同。如果您使用這些組件中的一打,那么您很容易得到一打配置文件來保持同步。我在這里的建議是,始終提供一種使用編程接口輕松完成所有配置的方法,然后將單獨的配置文件視為可選特性。您可以輕松地構(gòu)建配置文件處理來使用編程接口。如果正在編寫組件,則將其留給用戶決定是使用編程接口、配置文件格式,還是編寫自己的自定義配置文件格式并將其綁定到編程接口。

Separating Configuration from Use

所有這些中的重要問題是確保服務(wù)的配置與它們的使用分離。實際上,這是一個基本的設(shè)計原則,它將接口與實現(xiàn)分離開來。在面向?qū)ο蟪绦蛑校敆l件邏輯決定要實例化哪個類,然后通過多態(tài)性(而不是通過重復(fù)的條件代碼)對該條件進行未來的評估時,我們就會看到這種情況。如果這種分離在單個代碼庫中有用,那么在使用組件和服務(wù)等外部元素時尤其重要。第一個問題是,是否希望將實現(xiàn)類的選擇推遲到特定的部署。如果是這樣,你需要使用一些插件的實現(xiàn)。一旦您使用了插件,那么插件的組裝就必須與應(yīng)用程序的其他部分分開進行,這樣您就可以輕松地為不同的部署替換不同的配置。如何做到這一點是次要的。這種配置機制既可以配置服務(wù)定位器,也可以使用注入直接配置對象。

Some further issues

在本文中,我主要討論了使用依賴項注入和服務(wù)定位器進行服務(wù)配置的基本問題。還有其他一些話題也值得關(guān)注,但我還沒有時間深入探討。特別是生命周期行為的問題。有些組件具有不同的生命周期事件:例如,停止和啟動。另一個問題是,人們對在這些容器中使用面向方面的思想越來越感興趣。雖然我目前還沒有在本文中考慮過這些材料,但是我確實希望通過擴展本文或編寫另一篇文章來對此進行更多的討論。通過查看致力于輕量級容器的web站點,您可以了解更多關(guān)于這些想法的信息。從picocontainer和spring web站點進行瀏覽將使您對這些問題進行更多的討論,并開始討論一些更深入的問題。

當前大量的輕量級容器都有一個共同的底層模式來進行服務(wù)組裝——依賴注入器模式。依賴項注入是服務(wù)定位器的一個有用的替代方法。當構(gòu)建應(yīng)用程序類時,這兩個類大致相當,但是我認為Service Locator有一點優(yōu)勢,因為它的行為更直接。但是,如果要構(gòu)建用于多個應(yīng)用程序的類,則依賴項注入是更好的選擇。如果使用依賴項注入,則有許多樣式可供選擇。我建議您遵循構(gòu)造函數(shù)注入,除非您遇到這種方法的特定問題之一,在這種情況下切換到setter注入。如果選擇構(gòu)建或獲取容器,請尋找同時支持構(gòu)造函數(shù)和setter注入的容器。服務(wù)定位器和依賴項注入之間的選擇沒有將服務(wù)配置與應(yīng)用程序中的服務(wù)使用分離開來的原則重要。

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容