換一種角度:從架構層面來看設計模式
大部分講解設計模式的書或者文章,都是從代碼層面來講解設計模式,看的時候都懂,但是到真正用的時候,還是理不清、想不明。
本文嘗試從架構層面來聊一聊設計模式。通過將使用設計模式的代碼和不使用設計模式的代碼分別放到架構中,來看看設計模式對架構所產生的影響。
一般模式講解套路
一般講解設計模式的套路是:
- 說明模式的意圖
- 說明模式的適用場景
- 給出模式的類結構
- 給出對應的代碼示例
以策略模式為例:
意圖:定義一系列的算法,把它們一個個封裝起來, 并且使它們可相互替換。本模式使得算法可獨立于使用它的客戶而變化。
適用性:
- 許多相關的類僅僅是行為有異。「策略」提供了一種用多個行為中的一個行為來配置一個類的方法。
- 需要使用一個算法的不同變體。例如,你可能會定義一些反映不同的空間/時間權衡的算法。當這些變體實現為一個算法的類層次時,可以使用策略模式。
- 算法使用客戶不應該知道的數據。可使用策略模式以避免暴露復雜的、與算法相關的數據結構。
- 一個類定義了多種行為, 并且這些行為在這個類的操作中以多個條件語句的形式出現。將相關的條件分支移入它們各自的Strategy類中以代替這些條件語句。
類結構:

示例代碼:
- public class Context {
- //持有一個具體策略的對象
- private Strategy strategy;
- /**
- * 構造函數,傳入一個具體策略對象
- * @param strategy 具體策略對象
- */
- public Context(Strategy strategy){
- this.strategy = strategy;
- }
- /**
- * 策略方法
- */
- public void invoke(){
- strategy.doInvoke();
- }
- }
- public interface Strategy {
- /**
- * 策略方法
- */
- public void doInvoke();
- }
- public class StrategyA implements Strategy {
- @Override
- public void doInvoke() {
- System.out.println("InvokeA");
- }
- }
- public class StrategyB implements Strategy {
- @Override
- public void doInvoke() {
- System.out.println("InvokeB");
- }
- }
從上面的講解,你能理解策略模式嗎?你是否有如下的一些疑問?
- 使用策略模式和我直接寫if-else具體的優勢在哪里?
- if-else不是挺簡單的,為什么要多寫這么多的類?
- 如何將Strategy給設置到Context中?
- 我該如何判斷將哪個實現設置給Context?還是ifelse?!那拆成這么多的類不是脫褲子放屁嗎?
將模式放入架構中
產生這些疑問的原因,是我們在孤立的看設計模式,而沒有把設計模式放到實際的場景中。
當我們將其放到實際項目中時,我們實際是需要一個客戶端來組裝和調用這個設計模式的,如下圖所示:

- public class Client {
- public static void main(String[] args) {
- Strategy strategy;
- if("A".equals(args[0])) {
- strategy = new StrategyA();
- } else {
- strategy = new StrategyB();
- }
- Context context = new Context(strategy);
- context.invoke();
- }
- }
作為比較,這里也給出直接使用ifelse時的結構和代碼:

- public class Client {
- public static void main(String[] args) {
- Context context = new Context(args[0]);
- context.invoke();
- }
- }
- public class Context {
- public void invoke(String type) {
- if("A".equals(type)) {
- System.out.println("InvokeA");
- } else if("B".equals(type)) {
- System.out.println("InvokeB");
- }
- }
- }
乍看之下,使用ifelse更加的簡單明了,不過別急,下面我們來對比一下兩種實現方式的區別,來具體看看設計模式所帶來的優勢。
邊界不同
首先,使用策略模式使得架構的邊界與使用ifelse編碼方式的架構的邊界不同。策略模式將代碼分成了三部分,這里稱為:
- 調用層:將下層的業務邏輯組裝起來,形成完整的可執行流程
- 邏輯層:具體的業務邏輯流程
- 實現層:實現業務邏輯中可替換邏輯的具體實現

而ifelse將代碼分成了兩部分:
- 調用層:將下層的業務邏輯組裝起來,形成完整的可執行流程
- 邏輯層:具體的業務邏輯流程及具體邏輯

解耦
在ifelse實現中,「邏輯流程」和「邏輯實現」是硬編碼在一起的,明顯的緊耦合。而策略模式將「邏輯流程」和「邏輯實現」拆分開,對其進行了解耦。
解耦后,「邏輯流程」和「邏輯實現」就可以獨立的進化,而不會相互影響。
獨立進化
假設現在要調整業務流程。對于策略模式來說,需要修改的是「邏輯層」;而對于ifelse來說,需要修改的也是「邏輯層」。
假設現在要新增一個策略。對于策略模式來說,需要修改的是「實現層」;而對于ifelse來說,需要修改的還是「邏輯層」。
在軟件開發中,有一個原則叫單一職責原則,它不僅僅是針對類或方法的,它也適用于包、模塊甚至子系統。
對應到這里,你會發現,ifelse的實現方式違背了單一職責原則。使用ifelse實現,使得邏輯層的職責不單一了。當業務流程需要調整時,需要調整邏輯層的代碼;當具體的業務邏輯實現需要調整時,也需要調整邏輯層。
而策略模式將業務流程和具體的業務邏輯拆分到了不同的層內,使得每一層的職責相對的單一,也就可以獨立的進化。
對象聚集
我們重新來觀察一下策略模式的架構圖,再對照上面的調用代碼,你有沒有發現缺少了點什么?
在Client中,我們要根據參數判定來實例化了StategyA或StategyB對象。也就是說,「調用層」使用了「實現層」的代碼,實際調用邏輯應該是這樣的:

可以看到,Client與StategyA和StategyB是強依賴的。這會導致兩個問題:
- 對象分散:如果StategyA或StategyB的實例化方法需要調整,所有實例化代碼都需要進行調整。或者如果新增了StategyC,那么所有將Stategy設置到Context的相關代碼都需要調整。
- 穩定層依賴不穩定層:一般情況下,「實現層」的變動頻率較高;而對于「調用層」來說,調用流程確定后,基本就不會變化了。讓一個基本不變的層去強依賴一個頻繁變化的層,顯然是有問題的。
我們先來解決「對象分散」的問題,下一節來解決「穩定層依賴不穩定層」的問題!
對于「對象分散」的問題來說,創建型的設計模式基本能解決這個問題,對應到這里,可以直接使用工廠方法!

使用了工廠方法后,構建代碼被限制在了工廠方法內部,當策略對象的構造邏輯調整時,我們只需要調整對應的工廠方法就可以了。
依賴倒置
現在「調用層」只和「實現層」的StategyFactoryImpl有直接的關系,解決了「對象分散」的問題。但是即使只依賴一個類,調用層依然和實現層是強依賴關系。
該如何解決這個問題呢?我們需要依賴倒置。一般方法是使用接口,例如這里的「邏輯層」和「實現層」就是通過接口來實現了依賴倒置:「邏輯層」并不強依賴于「實現層」的任何一個類。箭頭方向都是從「實現層」指向「邏輯層」的,所以稱為依賴倒置

但是對于「調用層」來說,此方法并不適用,因為它需要實例化具體的對象。那我們該如何處理呢?
相信你已經想到了,就是我們一直在用的IOC!通過注入的方式,使得依賴倒置!我們可以直接替換掉工廠方法。

可以看到,通過依賴注入,使得「調用層」和「實現層」都依賴于「邏輯層」。由于「邏輯層」也是相對較穩定的,所以「調用層」也就不會頻繁的變化,現在需要變化的只有「實現層」了。
邏輯顯化
最后一個區別就是設計模式使得邏輯顯化。什么意思呢?
當你使用ifelse的時候,實際上你需要深入到具體的ifelse代碼,你才能知道它的具體邏輯是什么。
對于使用設計模式的代碼來說,我們回過頭來看上面的架構圖,從這張圖你就能看出來對應的邏輯了:
- 由StrategyFactory實例化所有Strategy的實現
- Client通過StrategyFactory獲取Strategy實例,并將其設置到Context中
- 由Context委托給具體的Strategy來執行具體的邏輯
至于具體的Strategy邏輯是什么樣子的,你可以通過類名或方法名來將其顯化出來!
總結
本文通過將使用設計模式的代碼和不使用設計模式的代碼分別放到架構中,對比設計模式對架構所產生的影響:
- 劃分邊界
- 解耦
- 獨立進化
- 對象聚集
- 依賴倒置
- 邏輯顯化