換一種角度:從架構(gòu)層面來看設(shè)計模式
大部分講解設(shè)計模式的書或者文章,都是從代碼層面來講解設(shè)計模式,看的時候都懂,但是到真正用的時候,還是理不清、想不明。
本文嘗試從架構(gòu)層面來聊一聊設(shè)計模式。通過將使用設(shè)計模式的代碼和不使用設(shè)計模式的代碼分別放到架構(gòu)中,來看看設(shè)計模式對架構(gòu)所產(chǎn)生的影響。
一般模式講解套路
一般講解設(shè)計模式的套路是:
- 說明模式的意圖
- 說明模式的適用場景
- 給出模式的類結(jié)構(gòu)
- 給出對應(yīng)的代碼示例
以策略模式為例:
意圖:定義一系列的算法,把它們一個個封裝起來, 并且使它們可相互替換。本模式使得算法可獨立于使用它的客戶而變化。
適用性:
- 許多相關(guān)的類僅僅是行為有異?!覆呗浴固峁┝艘环N用多個行為中的一個行為來配置一個類的方法。
- 需要使用一個算法的不同變體。例如,你可能會定義一些反映不同的空間/時間權(quán)衡的算法。當(dāng)這些變體實現(xiàn)為一個算法的類層次時,可以使用策略模式。
- 算法使用客戶不應(yīng)該知道的數(shù)據(jù)??墒褂貌呗阅J揭员苊獗┞稄?fù)雜的、與算法相關(guān)的數(shù)據(jù)結(jié)構(gòu)。
- 一個類定義了多種行為, 并且這些行為在這個類的操作中以多個條件語句的形式出現(xiàn)。將相關(guān)的條件分支移入它們各自的Strategy類中以代替這些條件語句。
類結(jié)構(gòu):

示例代碼:
- public class Context {
- //持有一個具體策略的對象
- private Strategy strategy;
- /**
- * 構(gòu)造函數(shù),傳入一個具體策略對象
- * @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具體的優(yōu)勢在哪里?
- if-else不是挺簡單的,為什么要多寫這么多的類?
- 如何將Strategy給設(shè)置到Context中?
- 我該如何判斷將哪個實現(xiàn)設(shè)置給Context?還是ifelse?!那拆成這么多的類不是脫褲子放屁嗎?
將模式放入架構(gòu)中
產(chǎn)生這些疑問的原因,是我們在孤立的看設(shè)計模式,而沒有把設(shè)計模式放到實際的場景中。
當(dāng)我們將其放到實際項目中時,我們實際是需要一個客戶端來組裝和調(diào)用這個設(shè)計模式的,如下圖所示:

- 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時的結(jié)構(gòu)和代碼:

- 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更加的簡單明了,不過別急,下面我們來對比一下兩種實現(xiàn)方式的區(qū)別,來具體看看設(shè)計模式所帶來的優(yōu)勢。
邊界不同
首先,使用策略模式使得架構(gòu)的邊界與使用ifelse編碼方式的架構(gòu)的邊界不同。策略模式將代碼分成了三部分,這里稱為:
- 調(diào)用層:將下層的業(yè)務(wù)邏輯組裝起來,形成完整的可執(zhí)行流程
- 邏輯層:具體的業(yè)務(wù)邏輯流程
- 實現(xiàn)層:實現(xiàn)業(yè)務(wù)邏輯中可替換邏輯的具體實現(xiàn)

而ifelse將代碼分成了兩部分:
- 調(diào)用層:將下層的業(yè)務(wù)邏輯組裝起來,形成完整的可執(zhí)行流程
- 邏輯層:具體的業(yè)務(wù)邏輯流程及具體邏輯

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

可以看到,Client與StategyA和StategyB是強依賴的。這會導(dǎo)致兩個問題:
- 對象分散:如果StategyA或StategyB的實例化方法需要調(diào)整,所有實例化代碼都需要進行調(diào)整?;蛘呷绻略隽薙tategyC,那么所有將Stategy設(shè)置到Context的相關(guān)代碼都需要調(diào)整。
- 穩(wěn)定層依賴不穩(wěn)定層:一般情況下,「實現(xiàn)層」的變動頻率較高;而對于「調(diào)用層」來說,調(diào)用流程確定后,基本就不會變化了。讓一個基本不變的層去強依賴一個頻繁變化的層,顯然是有問題的。
我們先來解決「對象分散」的問題,下一節(jié)來解決「穩(wěn)定層依賴不穩(wěn)定層」的問題!
對于「對象分散」的問題來說,創(chuàng)建型的設(shè)計模式基本能解決這個問題,對應(yīng)到這里,可以直接使用工廠方法!

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

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

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