反轉控制容器與依賴注入模式
在 Java 社群中,出現了一股輕量級容器的熱潮,有助於將不同專案的元件組裝成一個有凝聚力的應用程式。這些容器的基礎是一個共同的模式,說明它們如何執行配線,這個概念在一個非常通用的名稱「反轉控制」下被提及。在本文中,我將深入探討這個模式如何運作,使用更具體的名稱「依賴注入」,並將其與服務定位器替代方案進行對比。它們之間的選擇不如將設定與使用分開的原則重要。
2004 年 1 月 23 日
企業 Java 世界中令人興奮的事情之一就是建立主流 J2EE 技術的替代方案,其中大部分發生在開放原始碼中。其中許多是對主流 J2EE 世界中重量級複雜性的反應,但許多也在探索替代方案並提出創意的想法。要處理的常見問題是如何將不同的元素連接在一起:當它們是由彼此不太了解的不同團隊所建置時,你要如何將這個網頁控制器架構與那個資料庫介面後端結合在一起。許多架構都嘗試解決這個問題,而有幾個架構分支出來,提供從不同層組裝元件的一般功能。這些通常稱為輕量級容器,範例包括 PicoContainer 和 Spring。
這些容器的基礎是許多有趣的設計原則,這些原則不僅超越了這些特定容器,也超越了 Java 平台。在這裡,我想開始探索其中一些原則。我使用的範例是 Java,但就像我的大部分寫作一樣,這些原則同樣適用於其他 OO 環境,特別是 .NET。
元件與服務
將元素連接在一起的主題幾乎立刻就把我拖進圍繞服務和元件這些術語的棘手術語問題中。你可以輕鬆找到關於這些事物定義的冗長且相互矛盾的文章。對於我的目的,以下是這些過載術語我目前的用法。
我使用元件來表示一團軟體,其目的是由一個不受元件作者控制的應用程式使用,且不變更。我所謂的「不變更」是指使用中的應用程式不會變更元件的原始碼,儘管他們可能會透過元件作者允許的方式擴充元件,來變更元件的行為。
服務類似於元件,因為它是由外部應用程式使用的。主要的差異是我預期元件會在本地使用(想想 jar 檔案、組件、dll 或原始碼匯入)。服務會透過某個遠端介面遠端使用,可能是同步或非同步的(例如網頁服務、訊息系統、RPC 或 socket)。
我在這篇文章中大多使用服務,但許多相同的邏輯也可以套用於本地元件。事實上,你通常需要某種本地元件架構才能輕鬆存取遠端服務。但撰寫「元件或服務」讀起來和寫起來都很累,而服務目前更為流行。
一個簡單的範例
為了讓這一切更具體,我將使用一個執行中的範例來說明這一切。就像我的所有範例一樣,這是一個超簡單的範例;小到不真實,但希望足以讓你視覺化發生了什麼事,而不會陷入真實範例的泥沼中。
在這個範例中,我撰寫一個元件,提供由特定導演執導的電影清單。這個令人驚嘆的有用功能是由單一方法實作。
類別 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
方法完全獨立於所有電影的儲存方式。因此,方法所做的就是參照尋找器,而尋找器所做的就是知道如何回應 findAll
方法。我可以透過定義尋找器的介面來達成這一點。
public interface MovieFinder { List findAll(); }
現在,所有這些都解耦得很好,但在某個時間點,我必須提出具體類別來實際提出電影。在這個案例中,我將程式碼放在列示器類別的建構函式中。
類別 MovieLister...
private MovieFinder finder; public MovieLister() { finder = new ColonDelimitedMovieFinder("movies1.txt"); }
實作類別的名稱來自於我從冒號分隔的文字檔案取得清單。我將省略細節,畢竟重點只是有一些實作。
現在,如果我僅自己使用這個類別,這一切都很好。但是,當我的朋友們被對這個美妙功能的渴望淹沒,並想要我程式的一個副本時,會發生什麼事?如果他們也將電影清單儲存在稱為「movies1.txt」的冒號分隔文字檔案中,那麼一切都很好。如果他們的電影檔案有不同的名稱,那麼將檔案名稱放在屬性檔案中很容易。但是,如果他們有完全不同的電影清單儲存形式:SQL 資料庫、XML 檔案、網路服務或其他格式的文字檔案,該怎麼辦?在這種情況下,我們需要不同的類別來擷取該資料。現在,由於我已經定義了 MovieFinder
介面,這不會改變我的 moviesDirectedBy
方法。但我仍然需要一些方法來取得正確尋找器實作的執行個體。

圖 1:在列示器類別中使用簡單建立的相依性
圖 1 顯示此情況的相依性。MovieLister
類別相依於 MovieFinder
介面和實作。我們希望它只相依於介面,但我們該如何建立一個執行個體來使用呢?
在我的書 P of EAA 中,我們將此情況描述為 外掛程式。尋找器的實作類別不會在編譯時連結到程式中,因為我不知道我的朋友會使用什麼。我們希望我的列示器能與任何實作搭配使用,並在稍後某個時間點插入該實作,而這不在我的控制範圍內。問題是如何建立連結,讓我的列示器類別忽略實作類別,但仍能與執行個體對話以執行其工作。
將此擴充為一個真實系統,我們可能有數十種此類服務和元件。在每種情況下,我們都可以透過介面與這些元件對話(如果元件未考量介面,則使用轉接器)來抽象化我們對這些元件的使用方式。但如果我們希望以不同的方式部署此系統,則需要使用外掛程式來處理與這些服務的互動,以便我們可以在不同的部署中使用不同的實作。
因此,核心問題是如何將這些外掛程式組裝到應用程式中?這是這種類型的輕量級容器所面臨的主要問題之一,而它們普遍都使用控制反轉來解決此問題。
反轉控制
當這些容器談論它們如何如此有用,因為它們實作了「控制反轉」時,我感到非常困惑。控制反轉 是架構的常見特性,因此說這些輕量級容器很特別,因為它們使用控制反轉,就像說我的車很特別,因為它有輪子一樣。
問題是:「它們反轉了控制的哪個面向?」當我第一次遇到控制反轉時,它是在使用者介面的主要控制中。早期的使用者介面是由應用程式控制的。你會有一系列指令,例如「輸入姓名」、「輸入地址」;你的程式會驅動提示並取得每個提示的回應。在圖形(或甚至是基於螢幕)的使用者介面中,使用者介面架構會包含此主迴圈,而你的程式則提供螢幕上各種欄位的事件處理常式。程式的主要控制被反轉,從你移轉到架構。
對於這一類新型的容器,反轉處理的是它們如何查詢外掛程式實作。在我的簡單範例中,列示器會透過直接實例化查詢器實作來查詢查詢器實作。這會讓查詢器無法成為外掛程式。這些容器所採用的方法是確保外掛程式的任何使用者都遵循某項慣例,讓獨立的組裝模組可以將實作注入到列示器中。
因此,我認為我們需要為此模式取一個更明確的名稱。控制反轉是一個太過通用的術語,所以人們會覺得混淆。因此,在與許多 IoC 倡導者討論後,我們決定採用「依賴性注入」這個名稱。
我將從說明依賴性注入的各種形式開始,但我現在要指出,這並非從應用程式類別移除依賴性到外掛程式實作的唯一方法。你可以使用其他模式來執行此動作,那就是服務定位器,而我將在說明完依賴性注入後討論這個模式。
依賴注入的形式
依賴性注入的基本概念是有一個獨立的物件(組裝器),它會使用查詢器介面的適當實作來填充列示器類別中的欄位,進而產生類似於 圖 2 的依賴性圖表。

圖 2:依賴性注入器的依賴性
依賴性注入有三大主要類型。我為它們使用的名稱分別是建構函式注入、設定器注入和介面注入。如果你在目前關於控制反轉的討論中讀到這些內容,你會聽到它們分別稱為第 1 型 IoC(介面注入)、第 2 型 IoC(設定器注入)和第 3 型 IoC(建構函式注入)。我發現數字名稱比較難記,這就是我使用這裡這些名稱的原因。
使用 PicoContainer 的建構函式注入
我將從說明如何使用稱為 PicoContainer 的輕量級容器來執行此注入開始。我從這裡開始,主要是因為我在 Thoughtworks 的幾位同事都非常積極參與 PicoContainer 的開發(是的,這是一種企業裙帶關係)。
PicoContainer 使用建構函式來決定如何將查詢器實作注入到列示器類別中。為執行此動作,電影列示器類別需要宣告一個建構函式,其中包含它需要注入的所有內容。
類別 MovieLister...
public MovieLister(MovieFinder finder) { this.finder = finder; }
查詢器本身也會由 Pico 容器管理,因此容器會將文字檔案的檔名注入到查詢器中。
class ColonMovieFinder...
public ColonMovieFinder(String filename) { this.filename = filename; }
然後需要告知 Pico 容器要將哪個實作類別與每個介面關聯,以及要將哪個字串注入到查詢器中。
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; }
此組態程式碼通常會設定在不同的類別中。對於我們的範例,每個使用我的列示器的朋友都可以在他們自己的某個設定類別中撰寫適當的組態程式碼。當然,將此類型的組態資訊保存在獨立的組態檔案中是很常見的。你可以撰寫一個類別來讀取組態檔案,並適當地設定容器。雖然 PicoContainer 本身不包含此功能,但有一個密切相關的專案稱為 NanoContainer,它提供適當的包裝程式,讓你擁有 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()); }
儘管在此範例中我使用了建構函數注入,PicoContainer 也支援設定值注入,儘管其開發人員較偏好建構函數注入。
使用 Spring 的設定器注入
Spring 架構 是廣泛用於企業 Java 開發的架構。它包含交易、持久性架構、Web 應用程式開發和 JDBC 的抽象層。與 PicoContainer 一樣,它支援建構函數和設定值注入,但其開發人員傾向於偏好設定值注入,這使得它成為此範例的適當選擇。
為了讓我的電影清單接受注入,我為該服務定義設定方法
類別 MovieLister...
private MovieFinder finder; public void setFinder(MovieFinder finder) { this.finder = finder; }
類似地,我為檔案名稱定義設定值。
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 = 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()); }
介面注入
第三個注入技術是定義和使用介面進行注入。Avalon 是在某些地方使用此技術的架構範例。稍後我將進一步說明,但在這個案例中,我將使用一些簡單的範例程式碼。
使用此技術,我從定義一個介面開始,我將使用它透過介面進行注入。以下是將電影尋找器注入物件的介面。
public interface InjectFinder { void injectFinder(MovieFinder finder); }
此介面將由提供 MovieFinder 介面的人員定義。任何想要使用尋找器的類別,例如清單,都需要實作它。
class MovieLister implements InjectFinder
public void injectFinder(MovieFinder finder) { this.finder = finder; }
我使用類似的方法將檔案名稱注入尋找器實作。
public interface InjectFinderFilename { void injectFilename (String filename); }
class ColonMovieFinder implements MovieFinder, InjectFinderFilename...
public void injectFilename(String filename) { this.filename = filename; }
然後,像往常一樣,我需要一些組態程式碼來連接實作。為了簡化起見,我將在程式碼中執行此操作。
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); }
新的步驟是註冊將注入依賴元件的注入器。每個注入介面都需要一些程式碼來注入依賴物件。在此,我透過向容器註冊注入器物件來執行此操作。每個注入器物件都實作注入器介面。
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); }
當依賴項是為此容器撰寫的類別時,元件實作注入器介面本身是有道理的,就像我在這裡使用電影尋找器所做的那樣。對於通用類別,例如字串,我在組態程式碼中使用內部類別。
類別 ColonMovieFinder 實作 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"); } }
然後測試使用容器。
類別 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()); }
容器使用宣告的注入介面找出相依關係和注入器,以注入正確的相依項。(我這裡所做的特定容器實作對技術來說並不重要,而且我不會顯示它,因為你只會笑而已。)
使用服務定位器
相依注入器的主要好處是,它移除了 MovieLister
類別對具體 MovieFinder
實作的相依關係。這允許我將清單提供給朋友,並讓他們為自己的環境插入合適的實作。注入不是打破這種相依關係的唯一方法,另一種方法是使用 服務定位器。
服務定位器的基本概念是有一個物件知道如何取得應用程式可能需要的全部服務。因此,這個應用程式的服務定位器會有一個方法,在需要時傳回電影尋找器。當然,這只是將負擔轉移了一點,我們仍然必須將定位器放入清單中,導致 圖 3 的相依關係。

圖 3:服務定位器的相依關係
在這個案例中,我將服務定位器用作單例 註冊表。然後清單可以在實例化時使用它來取得尋找器。
類別 MovieLister...
MovieFinder finder = ServiceLocator.movieFinder();
類別 ServiceLocator...
public static MovieFinder movieFinder() { return soleInstance.movieFinder; } private static ServiceLocator soleInstance; private MovieFinder movieFinder;
與注入方法一樣,我們必須設定服務定位器。這裡我在程式碼中執行此操作,但使用從設定檔中讀取適當資料的機制並不困難。
class Tester...
private void configure() { ServiceLocator.load(new ServiceLocator(new ColonMovieFinder("movies1.txt"))); }
類別 ServiceLocator...
public static void load(ServiceLocator arg) { soleInstance = arg; } public ServiceLocator(MovieFinder movieFinder) { this.movieFinder = movieFinder; }
以下是測試程式碼。
class Tester...
public void testSimple() { configure(); MovieLister lister = new MovieLister(); Movie[] movies = lister.moviesDirectedBy("Sergio Leone"); assertEquals("Once Upon a Time in the West", movies[0].getTitle()); }
我經常聽到的抱怨是,這些類型的服務定位器是一件壞事,因為它們無法測試,因為你無法替換它們的實作。當然,你可以設計不良而陷入這種麻煩,但你無需如此。在這個案例中,服務定位器實例只是一個簡單的資料持有者。我可以輕鬆地使用服務的測試實作建立定位器。
對於更精密的定位器,我可以建立服務定位器的子類別,並將該子類別傳遞到註冊表的類別變數中。我可以變更靜態方法,以呼叫實例上的方法,而不是直接存取實例變數。我可以透過使用執行緒特定儲存空間來提供執行緒特定定位器。所有這些都可以完成,而無需變更服務定位器的用戶端。
思考這一點的方法是,服務定位器是一個註冊表,而不是單例。單例提供實作註冊表的簡單方法,但該實作決策很容易變更。
使用定位器的分離介面
上述簡單方法的一個問題是,MovieLister 依賴於完整的服務定位器類別,即使它只使用一個服務。我們可以使用 角色介面 來減少這種情況。這樣一來,清單可以宣告它需要的介面部分,而不是使用完整的服務定位器介面。
在這種情況下,清單的提供者也會提供一個定位器介面,它需要取得尋找器。
public interface MovieFinderLocator { public MovieFinder movieFinder();
定位器接著需要實作此介面來提供尋找器的存取權。
MovieFinderLocator locator = ServiceLocator.locator(); MovieFinder finder = locator.movieFinder();
public static ServiceLocator locator() { return soleInstance; } public MovieFinder movieFinder() { return movieFinder; } private static ServiceLocator soleInstance; private MovieFinder movieFinder;
您會注意到,由於我們想要使用介面,我們無法再透過靜態方法存取服務。我們必須使用類別來取得定位器執行個體,然後使用該執行個體取得我們需要的事物。
一個動態服務定位器
上述範例是靜態的,因為服務定位器類別有方法可供您需要的每項服務使用。這不是唯一的方法,您也可以建立一個動態服務定位器,讓您將需要的任何服務儲存其中,並在執行階段做出選擇。
在這種情況下,服務定位器使用一個對應於每項服務的映射,而非欄位,並提供一般方法來取得和載入服務。
類別 ServiceLocator...
private static ServiceLocator soleInstance; public static void load(ServiceLocator arg) { soleInstance = arg; } private Map services = new HashMap(); public static Object getService(String key){ return soleInstance.services.get(key); } public void loadService (String key, Object service) { services.put(key, service); }
設定包括使用適當的鍵載入服務。
class Tester...
private void configure() { ServiceLocator locator = new ServiceLocator(); locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt")); ServiceLocator.load(locator); }
我使用相同的鍵字串使用服務。
類別 MovieLister...
MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");
整體而言,我不喜歡這種方法。雖然它確實很靈活,但它並不明確。我找出如何接觸服務的唯一方法是透過文字鍵。我偏好明確的方法,因為透過查看介面定義,可以更輕鬆地找出它們在哪裡。
使用 Avalon 同時使用定位器和注入
依賴性注入和服務定位器不一定是不相容的概念。同時使用兩者的良好範例是 Avalon 架構。Avalon 使用服務定位器,但使用注入來告訴元件在哪裡找到定位器。
Berin Loritsch 傳送給我這個使用 Avalon 的執行中的範例的簡單版本。
public class MyMovieLister implements MovieLister, Serviceable { private MovieFinder finder; public void service( ServiceManager manager ) throws ServiceException { finder = (MovieFinder)manager.lookup("finder"); }
服務方法是介面注入的範例,允許容器將服務管理員注入 MyMovieLister。服務管理員是服務定位器的範例。在此範例中,清單管理員不會將管理員儲存在欄位中,而是立即使用它來查詢尋找器,並將其儲存。
決定要使用哪個選項
到目前為止,我專注於說明我如何看待這些模式及其變化。現在我可以開始討論它們的優缺點,以協助找出何時使用哪一種模式。
服務定位器與依賴注入
基本的選擇在於服務定位器和依賴性注入之間。第一點是,這兩個實作都提供基本解耦,這在單純的範例中是缺少的 - 在這兩種情況下,應用程式碼都獨立於服務介面的具體實作。這兩個模式之間的重要差異在於如何將該實作提供給應用程式類別。使用服務定位器時,應用程式類別透過訊息明確地向定位器要求它。使用注入時,沒有明確的要求,服務會出現在應用程式類別中 - 因此控制反轉。
控制反轉是架構的常見功能,但它需要付出代價。它往往難以理解,而且在您嘗試除錯時會導致問題。因此,整體而言,除非有需要,否則我比較喜歡避免它。這並不是說它是一件壞事,只是我覺得它需要證明自己比更直接的替代方案好。
關鍵的差異在於,使用服務定位器時,服務的每個使用者都對定位器有依賴性。定位器可以隱藏對其他實作的依賴性,但您確實需要看到定位器。因此,定位器和注入器之間的決策取決於該依賴性是否會造成問題。
使用依賴性注入可以幫助您更輕鬆地查看元件依賴性。使用依賴性注入器,您只需查看注入機制,例如建構函式,即可看到依賴性。使用服務定位器,您必須搜尋原始碼以尋找對定位器的呼叫。具有尋找參考功能的現代 IDE 可以讓這項工作變得更容易,但它仍然不如查看建構函式或設定方法來得容易。
這在很大程度上取決於服務使用者的性質。如果您正在建構一個使用服務的各種類別的應用程式,那麼應用程式類別對定位器的依賴性並非什麼大問題。在我提供電影清單給朋友的範例中,使用服務定位器運作得很好。他們只需要將定位器設定為連接到正確的服務實作,無論是透過一些設定碼或設定檔。在這種情況下,我認為注入器的反轉沒有提供任何令人信服的東西。
如果清單是我提供給其他人撰寫的應用程式的元件,差異就會出現。在這種情況下,我對客戶將要使用的服務定位器的 API 了解不多。每個客戶可能都有自己不相容的服務定位器。我可以透過使用分離介面來解決其中一些問題。每個客戶都可以撰寫一個將我的介面與其定位器相符的轉接器,但無論如何,我仍然需要看到第一個定位器才能查詢我的特定介面。而且一旦轉接器出現,直接連接到定位器的簡單性就會開始下降。
由於使用注入器時,元件對注入器沒有依賴性,因此元件在設定後無法從注入器取得進一步的服務。
人們偏好依賴性注入的一個常見原因是它讓測試變得更容易。此處的重點是,要進行測試,您需要輕鬆地用存根或模擬替換真正的服務實作。然而,依賴性注入和服務定位器之間並沒有真正的差異:兩者都非常適合存根。我懷疑這個觀察來自於人們沒有努力確保他們的服務定位器可以輕鬆替換的專案。這正是持續測試有幫助的地方,如果您無法輕鬆地存根服務進行測試,這表示您的設計存在嚴重的問題。
當然,測試問題會因非常具有侵入性的元件環境而惡化,例如 Java 的 EJB 架構。我的看法是,這些類型的架構應該將它們對應用程式碼的影響降到最低,特別是不應該做會減慢編輯執行週期的動作。使用外掛程式來替換重量級元件對這個過程有很大的幫助,這對於測試驅動開發等實務至關重要。
因此,主要問題在於編寫程式碼的人員預期其程式碼會用於作者無法控制的應用程式中。在這些情況下,即使對服務定位器做最小的假設也會產生問題。
建構函式與設定器注入
對於服務組合,您必須始終遵循一些慣例才能將各項事物串聯在一起。注入的優點在於它需要非常簡單的慣例,至少對於建構函式和設定注入而言是如此。您不必在元件中執行任何奇怪的操作,而且注入器可以相當直接地設定所有內容。
介面注入更具侵入性,因為您必須撰寫大量介面才能將所有事物整理好。對於容器所需的一小組介面(例如 Avalon 的方法),這並不算太糟。但是,這對於組裝元件和相依性而言是一項繁重的工作,這就是目前輕量級容器採用設定和建構函式注入的原因。
設定和建構函式注入之間的選擇很有趣,因為它反映了物件導向程式設計中更常見的問題,即您應該在建構函式中填寫欄位,還是使用設定值來填寫。
我對物件的長期預設是盡可能在建構時建立有效的物件。這項建議可以追溯到 Kent Beck 的 Smalltalk 最佳實務模式:建構函式方法和建構函式參數方法。帶有參數的建構函式讓您清楚地說明在顯而易見的地方建立有效物件的意義。如果有超過一種方法可以執行此操作,請建立多個建構函式來顯示不同的組合。
建構函式初始化的另一個優點是,它允許您清楚地隱藏任何不可變的欄位,只要不提供設定值即可。我認為這很重要,如果某個項目不應該變更,那麼缺少設定值可以很好地傳達這一點。如果您使用設定值進行初始化,這可能會變成一個麻煩。(事實上,在這些情況下,我比較不喜歡使用一般的設定慣例,我比較喜歡像 initFoo
這樣的函式,以強調這是一個您只應該在建立時執行的動作。)
但是,在任何情況下都有例外。如果您有大量的建構函式參數,事情可能會看起來很凌亂,特別是在沒有關鍵字參數的語言中。的確,一個很長的建構函式通常表示一個過於繁忙的物件,應該將其分割,但在某些情況下,這就是您所需要的。
如果您有多種方法來建構有效的物件,則透過建構函式來顯示這一點會很困難,因為建構函式只能在參數的數量和類型上有所不同。這時,工廠方法就派上用場了,它們可以使用私有建構函式和設定項的組合來實作它們的工作。傳統工廠方法在元件組裝方面遇到的問題是,它們通常被視為靜態方法,而您無法在介面上擁有這些方法。您可以建立一個工廠類別,但這樣只會變成另一個服務實例。工廠服務通常是一個好策略,但您仍然必須使用此處的其中一項技術來實例化工廠。
如果您有簡單的參數(例如字串),建構函式也會受到影響。使用設定項注入,您可以為每個設定項指定一個名稱,以指出字串應執行的動作。使用建構函式,您只能依賴位置,這比較難以追蹤。
如果您有多個建構函式和繼承,那麼事情可能會變得特別尷尬。為了初始化所有內容,您必須提供建構函式以轉發到每個超類別建構函式,同時還要新增您自己的引數。這可能會導致建構函式爆炸得更厲害。
儘管有這些缺點,但我還是偏好從建構函式注入開始,但一旦我上面概述的問題開始成為問題,就要準備好切換到設定項注入。
這個問題導致提供依賴注入器作為其架構一部分的各種團隊之間產生了很多爭論。然而,似乎大多數建立這些架構的人已經意識到同時支援這兩種機制很重要,即使其中一種機制較受偏好。
程式碼或設定檔
一個獨立但經常混淆的問題是是否在 API 上使用組態檔或程式碼來連接服務。對於可能在許多地方部署的大多數應用程式,獨立的組態檔通常最有意義。幾乎所有時間這將是一個 XML 檔,這是合理的。然而,在某些情況下,使用程式碼來進行組裝會更容易。一種情況是您有一個沒有太多部署變化的簡單應用程式。在這種情況下,一點程式碼會比一個獨立的 XML 檔更清晰。
相反的情況是組裝相當複雜,涉及條件步驟。一旦您開始接近程式語言,XML 就會開始分解,最好使用具有所有語法來撰寫清晰程式的真實語言。然後,您撰寫一個執行組裝的建構函式類別。如果您有不同的建構函式場景,您可以提供多個建構函式類別,並使用一個簡單的組態檔在它們之間進行選擇。
我經常認為人們過於急於定義組態檔。程式語言通常會形成一個直接而強大的組態機制。現代語言可以輕鬆編譯小型組譯器,這些組譯器可用於組裝較大型系統的外掛程式。如果編譯很麻煩,那麼也有腳本語言可以很好地執行這項工作。
人們常說,設定檔不應該使用程式語言,因為它們需要由非程式設計師編輯。但這有多常發生?人們真的會期待非程式設計師去變更複雜伺服器端應用程式的交易隔離層級嗎?非語言設定檔只有在它們很簡單時才能運作良好。如果它們變得複雜,就該考慮使用適當的程式語言了。
我們目前在 Java 世界中看到的一件事是設定檔的混亂,其中每個元件都有自己的設定檔,而且與其他人的不同。如果你使用其中十幾個元件,你很容易就會得到十幾個需要同步的設定檔。
我這裡的建議是,永遠提供一種方法,可以使用程式介面輕鬆地進行所有設定,然後將單獨的設定檔視為一個選用功能。你可以輕鬆地建置設定檔處理程式,以使用程式介面。如果你正在撰寫一個元件,你可以讓使用者決定是否要使用程式介面、你的設定檔格式,或撰寫他們自己的自訂設定檔格式,並將其連結到程式介面
將設定與使用分開
所有這些中的重要問題是,確保服務的設定與其使用分開。事實上,這是一個基本的設計原則,與介面與實作的分離相符。當條件式邏輯決定要實例化哪個類別時,我們在物件導向程式中會看到這一點,然後對該條件式的未來評估會透過多型性而不是透過重複的條件式程式碼來完成。
如果這種分離在單一程式碼庫中是有用的,那麼當你使用元件和服務等外部元素時,它就特別重要。第一個問題是,你是否希望將實作類別的選擇延後到特定部署。如果是這樣,你需要使用一些外掛程式的實作。一旦你使用外掛程式,外掛程式的組裝就必須與應用程式的其他部分分開進行,這樣你才能輕鬆地為不同的部署替換不同的設定。你如何達成這一點是次要的。此設定機制可以設定服務定位器,或使用注入直接設定物件。
一些進一步的問題
在本文中,我專注於使用依賴注入和服務定位器進行服務組態的基本問題。還有其他一些主題也值得關注,但我目前還沒有時間深入探討。特別是生命週期行為的問題。有些元件有不同的生命週期事件:例如停止和啟動。另一個問題是使用面向切面的概念與這些容器結合起來的興趣日益濃厚。儘管我目前還沒有在本文中考慮到這項素材,但我確實希望透過延伸本文或撰寫另一篇文章來進一步探討這個主題。
您可以透過瀏覽專門討論輕量級容器的網站來進一步了解這些概念。從 picocontainer 和 spring 網站瀏覽將會引導您深入探討這些問題,並開始了解一些進一步的問題。
結論
當前的輕量級容器熱潮都有著一個共同的基本模式,說明它們如何進行服務組建 - 依賴注入模式。依賴注入是服務定位器的有用替代方案。在建構應用程式類別時,這兩者大致相當,但我認為服務定位器由於其更直接的行為而略勝一籌。但是,如果您要建構供多個應用程式使用的類別,那麼依賴注入會是更好的選擇。
如果您使用依賴注入,則有許多樣式可供選擇。我建議您遵循建構函式注入,除非您遇到該方法的特定問題之一,這種情況下請切換到設定值注入。如果您選擇建構或取得容器,請尋找同時支援建構函式和設定值注入的容器。
在應用程式中選擇服務定位器和依賴注入的優先順序低於將服務組態與服務使用分開的原則。
致謝
誠摯感謝許多協助我撰寫本文的人。Rod Johnson、Paul Hammant、Joe Walnes、Aslak Hellesøy、Jon Tirsén 和 Bill Caputo 幫助我掌握這些概念,並對本文的早期草稿提出評論。Berin Loritsch 和 Hamilton Verissimo de Oliveira 提供了一些非常有用的建議,說明 Avalon 如何融入其中。Dave W Smith 持續提出問題,詢問我關於最初介面注入組態程式碼的問題,因此讓我面對它很愚蠢的事實。Gerry Lowry 寄給我許多拼寫錯誤修正 - 足以跨越感謝的門檻。
重大修訂
2004 年 1 月 23 日:重新製作介面注入範例的組態碼。
2004 年 1 月 16 日:新增 Avalon 定位器和注入的簡短範例。
2004 年 1 月 14 日:首次發佈