模塊化的樂趣
模塊間的緊耦合是一種很糟糕的設計,而耦合的最壞表現就是模塊間的循環依賴。幸運的是,有幾種方法可以用來消除循環依賴,分別是回調函數,代碼上移,代碼下移。 接下來,我會為大家展示一個小例子。示例中,我會分別使用上述幾種技術來消除循環依賴。
在消除循環依賴之後,我們會探索另外兩項技術,來達到依賴反轉和消除模塊之間依賴的目的。本示例的所有代碼都可以在Google Code下載,每個解決方法對應的代碼都包含有一個編譯腳本和一個簡單測試用例。一般你隻需要輸入 ant compile就可以執行編譯腳本,不過如果你想使用JarAnalyzer,那麼必須要有GraphViz。注意每個解決方法對應的代碼版本都具有相同的行為!
接下來的例子非常簡單。假設有這樣一個係統,包含有Customer和Bill兩個類,分別在兩個模塊中—cust.jar和bill.jar。同時有一個測試類PaymentTest測試兩者交互的行為,集成在billtest.jar模塊。最初的類圖顯示在下麵,需要引起注意圖中兩個類的雙向關聯。
隨著逐步的深入,我們會往係統中添加更多的類和抽象來提高模塊化程度。另外,我們會使用JarAnalyzer來描述模塊間的關係,同時也用來衡量設計的好壞。用JarAnalyzer生成的模塊結構如下圖所示,可以通過查看編譯腳本來了解JarAnalyzer的用法。回到正題,這篇文章中我們的目的是用上述三種方法來消除循環依賴,再之後會探索不同的途徑來構建非循環依賴的模塊。
上移
首先使用的方法叫上移。我們通過將導致依賴的因子(這裏是折扣的計算)上移到一個更高級別的模塊,來達到消除循環依賴的目的。然而在此之前,我們要先弄清楚這個例子中為什麼會存在循環依賴。具體原因如下:
一個Customer擁有多個bill實例,當bill對象的pay方法被調用的時候,需要先去判斷是否有折扣。但是,計算折扣的方法是在Customer類中,而不是Bill。因此,Bill類需要調用Customer的方法來計算合適的折扣。可以這樣來思考這個問題...Customer代表一個付款人,而我們和每個付款人協商折扣。所以折扣的計算是封裝在Customer中的。
為了打破這個依賴,我們將導致依賴的因素上移到一個更高級別的類-CustomerMediator。Mediator類將計算折扣的細節封裝起來,並傳給Bill類。了解這次修改的最好方法就是看修改後的PaymentTest類。我已經修改好了編譯腳本,並且將Mediator打包成jar。但如果在深入了解這個類結構後,你可能會質疑為什麼不直接把折扣數從Customer類傳給Bill類。不用擔心,這個例子隻是初步的設計,並不是解決此類問題的最好方法。我們要知道的就是,這個方法的核心是將依賴上移到mediator模塊,來達到消除循環依賴的目的。
下移
一個解決此類特定循環依賴問題的更好方法是下移(這裏Customer和Bill間有組合關係)。通過下移,我們將導致依賴的因子下移到一個更低級別的模塊,這正好和上移 相反。我們引入一個DiscountCalculator類,用來傳遞給Bill類,修改後的PaymentTest類會生成DiscountCalculator對象並將其傳進來。因為Customer類知道怎麼計算折扣,所以由Customer類提供生成DiscountCalculator的工廠方法。新的類結構圖如下所示。
接下來我們會修改編譯腳本將DiscountCalcultor打包生成calc.jar,最終的模塊結構圖如下所示。
通過分析你會發現下移比上移在處理這種循環依賴的問題上顯得更為合理,但是關鍵的區別是什麼呢?使用上移我可以單獨部署cust.jar和bill.jar。而雖然下移更為合理,但是如果要部署cust.jar或者bill.jar,也必須依賴calc.jar。可行的的解決方案總是會和具體問題關聯,而理想的解決方案是在整個開發周期中具有靈活擴展性。
回調
回調類似於觀察者模式,我們將DiscountCalculator類重構為接口,並讓Customer實現這個接口。新的類結構圖如下所示。
在這個例子中,回調類似於下移和最初版本的組合,Customer作為DiscountCalculator類型被傳給Bill。與下移中DiscountCalculator類被封裝在一個單獨的模塊不同的是,現在我們把它放在bill模塊中。需要注意的是,它不能放在cust模塊,這會引入循環依賴。新的類結構圖如下所示,有點像消除循環依賴的最初版本。
依賴反轉
接下來我們討論一下模塊關係。雖然回調看上去是最合理的解決方法,但如果我們想單獨部署cust模塊而不依賴bill模塊呢?回調並不能做到這一點,不過通過一些小技巧,cust和bill模塊的依賴關係能夠被反轉。
首先,將Bill類重構為接口,接下來為了避免分離包(同一個包中的不同類被打包到不同的模塊),我將Bill類和Customer類放在同一個包。新的類圖如下所示。
反轉後的模塊結構圖如下所示 。
消除依賴
依賴反轉滿足了我們這樣的需求,獨立部署cust模塊而不用依賴bill模塊。不過現在,我想探索基於獨立測試模塊需求的解決方法。在依賴反轉之後,我能夠獨立的測試cust模塊,但是如果我想同時獨立測試(或者部署)兩個模塊呢?為了達到這個目的,需要徹底消除兩者之間的關聯。
事實證明,在使用了依賴反轉(大多是抽象耦合)後,類結構變得非常靈活。我隻需要簡單地把兩個接口-Bill和DiscountCalculator-分別封裝起來,不需要額外的修改。
我將它們移到一個新的包base,一樣地,修改編譯腳本將這兩個接口打包到base模塊。至此,我們成功地消除了bill模塊和cust模塊的關聯,模塊結構如下所示。
總結
從最開始的兩個存在循環依賴的模塊,到最後模塊之間沒有任何依賴的模塊結構,我們取得了很大的進步。 模塊之間沒有依賴,就意味著模塊可以獨立測試和部署。如果你關注我的博客,你應該知道我已經寫了大量的文章,關於權衡靈活性和複雜性,可用和重用,以及其他架構和設計方麵的。我也希望這個小例子能說清楚這些裏麵的部分概念。
最後提示,為了更深入地了解這樣設計的目的,以及從對象層次上去理解為什麼要這樣做,我希望你能親自運行每個工程的編譯腳本,並查看在stats目錄下的dependencies.html文件。當然,你需要確保JarAnalyzer正確運行,而JarAnalyzer需要依賴GraphViz。如你所見,相比原始的版本,最終的版本在設計質量上有著顯著的提升。
最後更新:2017-05-23 10:02:31