讀古今文學網 > Java程序員修煉之道 > 3.1 知識注入:理解IoC和DI >

3.1 知識注入:理解IoC和DI

為什麼需要瞭解控制反轉(IoC、依賴注入(DI)以及它們的基本原理?對於這個問題,仁者見仁智者見智。如果你在知名的問答網站programmers.stackexchange.com上問這個問題,肯定會得到很多不同的答案!

你可能只是剛開始使用不同的DI框架並學習網上的示例,但如果你能夠掌握對像關係映射(Object Relational Mapping,ORM)框架,比如Hibernate,你就可以變成編程高手。

本節首先介紹核心術語IoC和DI背後的一些原理,並探討使用這一範式的好處。為了讓這些概念不至於那麼抽像,我們會以HollywoodService為例展示它的轉變過程——從自己構造依賴項變成被注入依賴項。

我們先從IoC開始,這個術語經常被(錯誤地)和DI互換使用。1

1 從字面上來看,IoC是指一種機制,使用這種機制的用例很多,實現方式也很多。DI只是其中一種具體用例的具體實現方式。但因為DI非常流行,所以人們經常誤以為IoC就是DI,並且認為DI這種叫法比IoC更貼切。這是來自stackoverflow的更全面解釋(英文):http://stackoverflow.com/questions/6550700/inversion-of-control-vs-dependency-injection。——譯者注

3.1.1 控制反轉

在使用非IoC範式編程時,程序邏輯的流程通常是由一個功能中心來控制的。如果設計得好,這個功能中心會調用各個可重用對像中的方法執行特定的功能。

使用IoC,這個「中心控制」的設計原則會被反轉過來。調用者的代碼處理程序的執行順序,而程序邏輯則被封裝在接受調用的子流程中。

IoC也被稱為好萊塢原則,其思想可以歸結為會有另一段代碼擁有最初的控制線程,並且由它來調用你的代碼,而不是由你的代碼調用它。

好萊塢原則——「不要給我們打電話,我們會打給你」

好萊塢經紀人總是給人打電話,而不讓別人打給他們!如果你曾經跟好萊塢經紀人提議,在明年夏天籌劃一個「讓Java程序員成為拯救世界的英雄」的大片,你也許會深諳其道。

換一種方式來看IoC,回想一下視頻遊戲Zork(http://en.wikipedia.org/wiki/Zork)用戶界面的發展過程,從早期由命令行中的文本控制到如今用圖形用戶界面控制。

在命令行版本中,用戶界面只有一個空白提示符,等著用戶輸入。當用戶輸入「向東」或者「Grue,快逃」的指令後,遊戲的主應用邏輯會調用恰當的事件處理器來處理這些指令,並返回結果。這裡的關鍵點是應用邏輯要控制調用哪個事件處理器。

而在GUI版本中,IoC開始發揮作用。由GUI框架來控制調用事件處理器,而不是由應用邏輯。當用戶點擊了一個動作,比如「向東」時,GUI框架會直接調用對應的事件處理器,而應用邏輯可以把重點放在處理動作上。

程序的主控被反轉了,將控制權從應用邏輯中轉移到GUI框架。1

1 程序中出現了專門用來實現調用和控制邏輯的GUI框架,應用邏輯中的代碼只需關注應用請求的處理。——譯者注

IoC有幾種不同的實現,包括工廠模式、服務定位器模式,當然,還有依賴注入。這一術語最初由Martin Fowler在「控制反轉容器和依賴注入模式」2中提出,然後迅速傳遍大街小巷,反響強烈。

2 在Martin Fowler的網站http://martinfowler.com/中搜索Dependency Injection,你就可以找到這篇文章。

3.1.2 依賴注入

依賴注入是IoC的一種特定形態,是指尋找依賴項的過程不在當前執行代碼的直接控制之下。雖然你也可以自己編寫代碼實現依賴注入機制,但大多數開發人員都會使用自帶IoC容器的第三方DI框架,比如Guice。

注意 可以把IoC容器看做運行時環境。Java中為依賴注入提供的容器有Guice、Spring和PicoContainer。

IoC容器可以提供實用的服務,比如確保一個可重用的依賴項會被配置成單例模式。我們在3.3節介紹Guice時會探討它的一些服務。

提示 把依賴項注入對象的方法有很多種。可以用專門的DI框架,但也可以不這麼做!顯式地創建對像實例(依賴項)並把它們傳入對像中也可以和框架注入做的一樣好。1

1 感謝Thiago Arrais(http://stackoverflow.com/users/17801/thiago-arrais)提供了這個提示。

與很多編程範式一樣,理解使用DI的原因很重要。我們在表3-1中總結了它的主要好處。

表3-1 DI的好處

好處描述 例子 松耦合 你的代碼不再緊緊地綁定到依賴項的創建上了。如果能與面向接口編程的技術相結合,意味著你的代碼再也不用緊緊地綁定到依賴項的具體實現上了 HollywoodService對像不再需要創建它所需的SpreadsheetAgentFinder對象,而是使用從外部傳入的對象。如果面向接口編程,HollywoodService可以接受任何類型的AgentFinder傳入 易測性 作為松耦合的延伸,還有個特殊的用例值得一提。為了測試,可以把測試替身2作為依賴項注入到對像中 你可以注入一個總是返回相同價格的虛設票價服務,而不是使用「真實」的價格服務,因為它是外部服務,而且有時無法訪問 更強的內聚性 你的代碼可以專注於執行自己的任務,不用為了載入和配置依賴項而分心。這樣還能增強代碼的可讀性 你的DAO對象可以專注於查詢工作,不用考慮JDBC驅動的細節 可重用組件 作為松耦合的延伸,你的代碼應用範圍會更加寬廣,那些可以提供自己特定實現的用戶都可以使用你的代碼 一個積極進取的軟件開發人員可能會賣給你一個LinkedIn代理人查找器 更輕盈的代碼 你的代碼不再需要跨層傳遞依賴項,而是可以在需要依賴項的地方直接注入 你不再需要把JDBC驅動的細節信息從service類往下傳遞,而是直接在真正需要它的DAO中直接注入這個驅動實例

2 第11章會詳細介紹測試替身。

把普通代碼改寫成依賴注入的代碼是掌握這些理論的最佳方法,所以我們進入下一節吧。

3.1.3 轉成DI

本節會重點介紹如何把不用IoC的代碼變成使用工廠(或服務定位器)模式的代碼,再變成使用DI的代碼。在這些轉變之後有一個共同的關鍵技術,即面向接口編程。使用面向接口編程,甚至可以在運行時更換對象。

注意 本節的目的是鞏固你對DI的理解。因此某些比較套路化的代碼被省略掉了。

假設你剛接手了一個小項目,要找出所有對Java開發人員比較友善的好萊塢經紀人。在下面的代碼中,AgentFinder接口有兩個實現類:SpreadSheetAgentFinderWebServiceAgentFinder

代碼清單3-1 接口AgentFinder及其實現類

public interface AgentFinder
{
  public List<Agent> findAllAgents;
}
public class SpreadsheetAgentFinder implements AgentFinder
{
  @Override
  public List<Agent> findAllAgents { .. } //很多實現細節
}
public class WebServiceAgentFinder implements AgentFinder
{
  @Override
  public List<Agent> findAllAgents { .. } //很多實現細節
}
  

為了使用經紀人查找器,項目中有個默認的HollywoodServic類,它會從SpreadsheetAgentFinder裡得到一個經紀人列表,並根據是否友善對他們進行過濾,最終返回友善的經紀人列表。如下面的代碼所示。

代碼清單3-2 HollywoodService——自己創建AgentFinder的具體實現類實例

public class HollywoodService
{
  public static List<Agent> getFriendlyAgents
  {
   AgentFinder finder = new SpreadsheetAgentFinder;//1使用SpreadsheetAgentFinder
   List<Agent> agents = finder.findAllAgents;//調用接口方法
   List<Agent> friendlyAgents = 
         filterAgents(agents, \"Java Developers\");
   return friendlyAgents;//返回友善的經紀人
  }
public static List<Agent> filterAgents(List<Agent> agents,
     String agentType)
{
   List<Agent> filteredAgents = new ArrayList<>;
   for (Agent agent:agents) {
     if (agent.getType.equals(\"Java Developers\")) {
        filteredAgents.add(agent);
          }
     }
   return filteredAgents;
   }
}
  

再看一遍上面代碼裡的HollywoodService,注意到了嗎?它被SpreadsheetAgentFinder這個AgentFinder的具體實現死死地黏上了1。

過去這種黏糊糊的實現對Java開發者來說司空見慣,不勝其煩!為了解決這些共性問題,設計模式應運而生。一開始,很多開發人員用工廠模式和服務定位器模式的各種變體來解決這類問題,它們全都是IoC的一種。

1. 使用工廠和/或服務定位器模式的HollywoodService

抽像工廠、工廠方法或服務定位器模式中的某個(或它們的某種組合)通常用來解決這種被依賴項黏上的問題。

注意 工廠方法和抽像工廠模式在Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides寫的《設計模式:可復用面向對像軟件的基礎》(Addison-Wesley Professional,1994)中有所論述。服務定位器模式出現在Deepak Alur、John Crupi和Dan Malks編寫的《J2EE核心模式》第二版(Prentice Hall,2003)中。

下面這個版本的HollywoodService類用AgentFinderFactory實現對AgentFinder的動態選擇。

代碼清單3-3 HollywoodService由工廠負責提供AgentFinder

public class HollywoodServiceWithFactory {

  public List<Agent>
         getFriendlyAgents(String agentFinderType)//1注入agentFinderType
  {
    /**2通過工廠實例獲取AgentFinder*/
    AgentFinderFactory factory = 
                        AgentFinderFactory.getInstance; 
    AgentFinder finder = 
                        factory.getAgentFinder(agentFinderType);
    List<Agent> agents = finder.findAllAgents;
    List<Agent> friendlyAgents = 
          filterAgents(agents, \"Java Developers\");
    return friendlyAgents;
  }
  public static List<Agent> filterAgents(List<Agent> agents,
      String agentType)
  {
     ...//與代碼清單3-2中的實現一樣
  }
}
  

如你所見,現在沒有特定的AgentFinder實現類來黏你了。注入agentFinderType1,讓AgentFinderFactory根據這一類型挑選令人滿意的AgentFinder供你享用2。

這和DI相當接近了,但還有兩個問題。

  • 代碼中注入的是一個引用憑據(agentFinderType),而不是真正實現AgentFinder的對象。
  • 方法getFriendlyAgents中還有獲取其依賴項的代碼,達不到只關注自身職能的理想狀態。

隨著開發人員編寫更清晰代碼的意願不斷增強,DI技術也越來越流行,正在逐步取代工廠和服務定位器模式。

2. 使用DI的HollywoodService

你可能已經猜出接下來重構代碼該做什麼了!下一步要讓AgentFinder直接提供所需的getFriendlyAgents方法。代碼如下所示:

代碼清單3-4 HollywoodService——純手工DI注入AgentFinder

public class HollywoodServiceWithDI
{
  public static List<Agent>
                emailFriendlyAgents(AgentFinder finder)//1注入AgentFinder
   {
     /**2執行查找邏輯*/
     List<Agent> agents = finder.findAllAgents; 
     List<Agent> friendlyAgents = 
         filterAgents(agents, \"Java Developers\");
     return friendlyAgents;
   }
   public static List<Agent> filterAgents(List<Agent> agents,
         String agentType)
   {
     ...//參見代碼清單3-2
   }
}
  

看看這個純手工打造的DI方案,AgentFinder被直接注入到getFriendlyAgents方法中1。現在這個getFriendlyAgents方法乾淨利落,只專注於純業務邏輯2。

不過這種手工打造DI的生產方式還是有讓人頭疼的地方。如何配置AgentFinder具體實現的問題並沒有解決,原本AgentFinderFactory要做的工作還是要找個地方完成。

所以,能夠真正讓我們脫離苦海的只有自帶IoC容器的DI框架。打個比方,DI框架就是把你的代碼包起來的運行時環境,在你需要時為你注入依賴項。

DI框架的優勢在於它可以隨時隨地為你的代碼提供依賴項。因為框架中有IoC容器,在運行時,你的代碼需要的所有依賴項都會在那裡準備好。

如果HollywoodService類使用標準的JSR-330註解(可以使用任何與JSR-330兼容的框架),那麼它會是什麼樣子?

3. 使用JSR-330 DI的HollywoodService

來看看這個例子的最終版本,用框架來執行DI操作。在這裡,DI框架用標準的JSR-330@Inject註解將依賴項直接注入到getFriendlyAgents方法中,代碼如下所示:

代碼清單3-5 HollywoodService——用JSR-330 DI注入AgentFinder

public class HollywoodServiceJSR330
{
  @Inject public static List<Agent> emailFriendlyAgents(AgentFinder finder)//1JSR-330注入AgentFinder
   {
     /**執行查找邏輯*/
    List<Agent> agents = finder.findAllAgent;
    List<Agent> friendlyAgents =
           filterAgents(agents, \"Java Developers\");
    return friendlyAgents;
   }
   public static List<Agent> filterAgents(List<Agent> agents,
       String agentType)
   {
     ...//參見代碼清單3-2
   }
}
  

現在AgentFinder的某個具體實現(比如WebServiceAgentFinder)類的實例是由支持JSR-330@Inject註釋的DI框架在運行時注入的1。

提示 儘管JSR-330註解可以在方法上注入依賴項,但通常只用於構造方法或set方法中。下一節會對這一規範做進一步探討。

讓我們結合代碼清單3-5中的HollywoodServiceJSR330 類再來重溫一下依賴注入對我們的幫助。

  • 松耦合——HollywoodService不再依賴於AgentFinder的具體類來完成工作。

  • 可測性——為了測試HollywoodService類,你可以注入一個返回固定數量經紀人的基本Java類(比如POJOAgentFinder),這在測試驅動的開發中被稱為存根類。這對於單元測試來說非常完美,因為你不再需要Web服務、電子錶格或其他第三方實現之類的東西。

  • 更強的內聚性——你的代碼不用再和工廠打交道,不用四下裡去抓依賴項,只需要執行業務邏輯。

  • 可重用的組件——假如有個開發人員想用你的API,現在需要注入一個他們定制的AgentFinder實現類,就說JDBCAgentFinder吧,想像一下他輕鬆愜意的表情吧。

  • 更輕盈的代碼——HollywoodServiceJSR330中的代碼量與最初的HollywoodService相比明顯減少了很多。

使用DI正在逐步成為優秀Java開發人員的標準實踐,幾個流行的容器都提供了優異的DI能力。然而就在不久之前,DI框架領域還是群雄割據,它們風格迥異,各行其是,使用IoC容器的配置標準都自成體系。即便遵循類似配置風格的框架(比如XML或Java註解),還是存在什麼是共通的註解和配置這個問題。

新的DI標準化方式(JSR-330)就是要解決這個問題。它對大多數Java DI框架的核心能力做了很好的匯總。因為有DI框架(比如Guice)的內部工作機製作為其堅實的基礎,所以接下來我們會深入探討這一標準化方式。