本文內容包括:
控制反轉(IoC)模式通常用于組件。本文描述了如何對方法簽名使用該模式,以減少組件間的耦合并改善性能。IBM Global Business Services 顧問 André Fachat 用兩個例子展示這種方法的靈活性。
控制反轉(IoC)和依賴項注入(DI)是兩種引起極大關注的模式(參見 參考資料)。它們主要用在所謂的 IoC 容器中,這些容器以其他組件的形式將依賴項注入到一個組件中。然而,這兩種模式并未定義這些依賴項組件方法的設計方式。在經典的設計中,這些方法中的值對象或數據傳輸對象用作方法參數并在需要復雜對象時返回值。
本文向您展示還可以對方法簽名使用 IoC,從而使方法與值對象解耦。為此,要把方法簽名中的值對象替換成接口。我會介紹該方法的一些應用場景。我經常使用這種模式,并發現借助它可以更好地分離組件之間的關注點。并且在運行時,它能減少對象創建和復制工作。
使用值對象作為方法參數
關于 IoC 已經有很多描述,所以此處只闡述其總體原則:組件將其使用的組件配置、本地化和生命周期方面 “外包” 出去。例如,數據訪問 bean 直接從某處 “獲取” JDBC 連接并簡單地使用該連接,而不是尋找一個 JDBC 數據源(配置和本地化),也許還要自己處理連接池(生命周期)。在 IoC 設置中,這些方面通常由一個 IoC 容器處理,比如,該容器通過調用 setter 方法將這些依賴項注入組件。
IoC 主要處理組件的生命周期。本文并不關注組件,而是關注組件提供的操作的方法參數。
請看下圖,這是典型的組件設置的依賴關系圖,其中有兩個依賴關系并使用了方法參數(參見圖 1)。這些方法參數定義成值對象,即不含邏輯只含數據值的對象。
圖 1. 使用值對象作為方法參數的組件依賴關系圖
在圖 1 中,Component1 依賴于 Component2 和 Component3 (根據 IoC),并分別調用 method2 和 method3。如果 Component1 直接 “了解” Component2 或 Component3 或只使用 Component2 和 Component3 實現的接口,那么這與我們的討論不相關。然而,通常方法參數基本都是對象而不是接口。
在這個設置中,當 Component1 調用 method2 時,它必須實例化 Value Object 2 并為其賦值。同樣,如果 Component1 調用 method3,它必須實例化 Value Object 3 并為其賦值。
現在假設 Component1 需要用相同的輸入數據調用 method2 和 method3 來獲取不同的輸出數據。例如,Component1 可以是訂單準備組件,method2 可以是決定交貨期的方法,method3 可以是決定價格的方法。這兩種方法需要相同的輸入并提供不同的輸出。
在本例中,使用值對象作為方法參數需要 Component1 為每個方法調用創建值對象,并主動地將所需值復制到值對象中。同樣,必須實例化這每個值對象,盡管這已經不像在 Java? 早期的版本中那么耗費資源,但仍然需要資源。這些行為都降低了性能。下面幾節將介紹如何優化性能。
使用接口改善這種情況
目標是防止在不同的值對象間復制值。為此,可以將方法參數定義為接口。這樣,只要對象實現該接口,調用組件就可以將其想使用的任何對象用作方法參數。
圖 2 顯示了新的依賴關系:
圖 2. 使用接口作為方法參數的組件依賴關系圖
依賴項方法將方法參數定義成接口。調用組件將實現這些接口的對象(故意不稱之為值對象)實例化,并在這兩個方法調用中將該對象用作方法參數。
以下例子突出顯示了該方法的一些優勢。
例子:定價和交貨期
再次假設 method1 決定訂單的交貨期,method2 決定價格。清單 1 是這些組件和方法的簡單定義:
清單 1. 使用接口作為方法參數的組件樣例
interface LeadtimeComponent { void getLeadtimes(List<LeadtimeItem> items) throws LeadtimeException; } interface LeadtimeItem { Long getArticleId(); BigDecimal getQuantity(); String getQuantityUnit();
void setLeadtimeInDays(Integer leadtime); }
interface PricingComponent { void getPrices(List<PriceItem> items) throws PricingException; } interface PriceItem { Long getArticleId(); BigDecimal getQuantity(); String getQuantityUnit();
void setPrice(BigDecimal price); void setPriceUnit(String currency); } |
請注意這兩個接口為檢索商品數據的方法定義了極為相似的方法簽名:getArticleId()、getQuantity() 和 getQuantityUnit()。還要注意組件方法沒有返回值;它們通過對參數對象(即接口)調用 setter 方法來修改提供的 “現成” 對象,以設置價格和交貨期。
這個方法簡化了管道模式 (參見 參考資料)的實現,在該模式下,數據通過 “管道” 從一個組件輸送到下一個組件,管道的一個階段(組件)使用該管道在前面的步驟中提供的數據。圖 3 顯示了使用管道模式的一個序列圖:
圖 3. 使用管道模式準備訂單的序列圖 在本例中,訂單準備過程首先從購物車中讀取商品 ID。然后從分類數據庫添加更多信息、檢索交貨期和價格(其中的價格依賴于交貨期)并存儲購物車中的其他信息,以便使用這些信息決定最終價格。如果讀取購物車所返回的條目對象實現了分類、交貨期和價格方法所要求的接口,則該過程不需要進行任何復制。
工廠方法
您可能想了解圖 3 中的 OrderDB 組件及其 readCart() 方法。確實,這是一個特殊情況。在前面的例子中,依賴項方法(如 getPrices(...))修改過的所有對象均已作為方法參數進行傳遞。當組件正在從數據庫中讀取數據時這是不可能的,因為在本例中,購物車中的條目數量在讀取前是未知的。
這里的解決方案是為要讀取的條目提供一個帶工廠方法的方法參數,如清單 2 所示:
清單 2:在方法參數中使用工廠方法
interface OrderDBComponent { void readCart(Cart cart) throws OrderDBException; } interface Cart { Long getCartId();
CartItem newItem(); void addItem(CartItem item); } interface CartItem { void setArticleId(Long articleId); ... } |
借助這個定義,OrderDB 組件從數據庫中讀取條目,并且對于讀取的每個條目,都使用 newItem() 方法從 Cart 對象中獲取新的條目對象(CartItem)。填充了從數據庫中讀取的值后,通過 addItem() 方法把 CartItem 添加到購物車中。請注意,向條目填充值后再將該條目添加到購物車中,能夠使購物車總是保持一致。
接口和方法不匹配
這個方法適合以下情況,即多個依賴項組件定義的參數接口是兼容的,從而能夠由相同的對象實現。在某些情況下可能出現不兼容,例如兩個接口使用不同的返回值類型定義同一個方法。必須小心設計這些方法,才能不引入這類不兼容情況。同樣,在設計這些方法時,還應確保當不同的接口有相同的方法簽名時,方法參數接口所定義的方法具有相同的語義。
然而即使存在不兼容,所有數據也并未丟失!適配器對象能夠將對象轉換為實現所需接口的對象,而不必復制涉及到的數據。盡管必須使用該方式來實例化適配器對象,但這還是避免了復制數據值。
另一個例子
還有一個例子能顯示此方法的靈活性。我編寫了一個針對特定對象模型的編輯器,但希望將模型實現和編輯器實現分離開來。因此,讓編輯器為可以編輯的模型定義接口。然后由真實的模型實現來實現清單 3 中的接口:
清單 3. 模型編輯器接口樣例
interface ModelEditor { void edit(Model model); } interface Model { ModelElement newElement(); ModelElement addElement(ModelElement element); } |
在這個(相當)精簡的定義中,可以看到模型的 addElement() 方法不僅把 ModelElement 當作參數,還返回一個 ModelElement 實例。返回的 ModelElement 是新添加的模型元素所替換的模型元素,如果沒有替換任何元素,則為 NULL。然后,將返回值存儲到一個撤銷命令中,這樣就能通過再次調用 addElement() 輕松地恢復該模型。同樣,addElement() 方法實現模型一致性檢驗并拒絕無效更改。
結束語
本文展示了 IoC 的一種具體形式,即應用于組件方法的參數而非組件。使用接口作為方法參數是上下文 IoC 的一種形式(這是 IoC 術語),應用于調用程序的依賴項。就像將依賴項組件(如 PriceComponent)注入到調用程序的組件(如 OrderPrepareComponent)中一樣,調用程序組件也將其依賴項對象(方法參數接口的實現)注入到依賴項組件的方法中。由于被調用的組件僅限于在參數接口中定義的方法,所以接口能夠確保作為參數提供的對象是一致的。小心地減少功能上所必需的方法的接口,就會降低組件之間的耦合。 |