架構師日記-深入理解軟體設計模式 | 京東雲技術團隊

京東雲開發者發表於2023-05-12

作者:京東零售 劉慧卿

一 設計模式與程式語言

1.1 什麼是設計模式

設計模式(Design pattern) :由軟體開發人員在軟體開發中面臨常見問題的解決方案,是經過長時間的試驗積累總結出來的,它使設計更加靈活和優雅,複用性更好。從實用的角度來看,它代表了某一類問題的最佳實踐。

設計模式到底解決了開發過程中的哪些難題呢,它又是如何來解決的呢?

其核心是:複用和解耦。使不穩定依賴於穩定、具體依賴於抽象,以此增強軟體設計適應變化的能力。

1.2 什麼是程式設計正規化

要探討設計模式和程式語言的關係,還得從程式設計正規化談起。程式設計正規化一詞最早來自 Robert Floyd 在 1979 年圖靈獎的頒獎演說,是程式設計師看待程式的觀點,代表了程式設計者認為程式應該如何被構建和執行的看法,與軟體建模方式和架構風格有緊密關係。

當前主流的程式設計正規化有三種:

1.結構化程式設計(structured programming)

2.物件導向程式設計(object-oriented programming)

3.函數語言程式設計(functional programming)

這幾種程式設計正規化之間的關係如下:

1.起初是非結構化程式設計,指令(goto指令)可以隨便跳轉,資料可以隨便引用。後來有了結構化程式設計,人們把 goto 語句去掉了,約束了指令的方向性,過程之間是單向的,但資料卻是可以全域性訪問的;

2.後來物件導向程式設計的時候,人們乾脆將資料與其緊密耦合的方法放在一個邏輯邊界內,約束了資料的作用域,靠關係來查詢;

3.到函數語言程式設計的時候,人們約束了資料的可變性,透過一系列函式的組合來描述資料,從源到目標對映規則的編排,中間它是無狀態的;

程式設計正規化是抽象的,程式語言是具體的。程式設計正規化是程式語言背後的思想,要透過程式語言來體現。C 語言的主流程式設計正規化是結構化程式設計,而 Java 語言的主流程式設計正規化是物件導向程式設計,後來 Java8 開始支援 Lambda 表示式,將函數語言程式設計正規化的內容融合進來,同時新誕生的語言一開始就支援多正規化,比如 Scala,Go 和 Rust 等。

從結構化程式設計到物件導向程式設計,再到函數語言程式設計,抽象程度越來越高(離圖靈機模型越來越遠),與領域問題的距離越來越近。直觀的來講,就是解決現實問題的效率提升了,靈活性和執行效率隨之有所下降。

設計模式無論用什麼語言實現都是可以的,然而由於語言的各自差異化特點,不是每種語言都完美或統一實現各種設計模式。比如Java裡面有策略模式,那是因為Java8之前不支援方法傳遞,不能把一個方法當作引數傳給別人,所以有了策略模式。而JavaScript等語言可以直接傳函式,就根本沒必要造一個策略模式出來。

1.3 什麼是多型特性

物件導向程式語言有三大特性:封裝、繼承和多型。

1.封裝即資訊隱藏或資料保護,“資料結構"透過暴露有限的訪問介面,授權外部僅能透過"資料結構"提供的方法(函式)來訪問其內部的資料;

2.繼承的好處是可以實現程式碼複用,但不應過度使用,如果繼承的層次過深就會導致程式碼可讀性和可維護性變差。 因此建議少用繼承而多用組合模式;

3.多型可以分為變數的多型,方法的多型,類的多型。通常強調的是類的多型,多型的實現是指子類可以替換父類,在實際程式碼執行過程中呼叫子類的方法實現;

多型可以說是物件導向中最重要的一個特性,是解決專案中緊偶合的問題,提高程式碼的可擴充套件性和可複用性的核心,是很多設計模式、設計原則、程式設計技巧的程式碼實現基礎。

多型比較直觀的理解就是去完成某個動作,當不同的物件去完成時會產生出不同的狀態,其作用範圍可以是方法的引數和方法的返回型別。

多型這種特性也需要程式語言提供特殊的語法機制來實現,Java 中多型可以透過"子類繼承父類+子類重寫父類方法+父類引用指向子類物件"的方式實現,還可以透過"介面語法"的方式實現。C++中則使用virtual(虛擬函式)關鍵字來實現。 像一些動態語言如 Python 也可以透過 duck-typing 的語法實現,另外 Go 語言中的"隱藏式介面"也算是 duck-typing。

Python 語言,實現多型示例如下:

class MyFile:
    def write(self):
        print('I write a message into file.')
        
class MyDB:
    def write(self):
        print('I write data into db. ')
        
def doIt(writer):
    writer.write()

def demo():
    myFile= MyFile()
    myDB = MyDB()
    doIt(myFile)
    doIt(myDB )

二 設計模式與架構模式

2.1 瞭解架構模式

對給定上下文的軟體架構中常見問題的一種通用的可複用的解決方案,可以為設計大型軟體系統的各個方面提供相應的指導。它不僅顯示了軟體需求和軟體結構之間的對應關係,而且指定了整個軟體系統的組織和拓撲結構,提供了一些設計決策的基本原理,常見的架構設計模式如下:

架構模式模式描述適用場景
分層模式 (Layered pattern)用於可分解為子任務的結構化程式,每個子任務都位於特定的抽象層級,每一層都為上一層提供服務。桌面應用程式; 電子商務web應用程式; 移動App;
客戶端-伺服器模式 (Client-server pattern)伺服器將向多個客戶端提供服務。客戶端從伺服器請求服務,伺服器向這些客戶端提供相關服務。電子郵件、文件共享和銀行等線上應用程式; 基於IPC的應用程式;
主從模式 (Master-slave pattern)主節點將工作分配給相同的從節點,並根據從節點返回的結果計算最終結果。資料庫主從複製; 程式內多執行緒排程;
管道-過濾器模式 (Pipe-filter pattern)用於構造生成和處理資料流的系統。每個處理步驟都包含一個過濾器元件。要處理的資料透過管道傳遞。這些管道可用於緩衝或同步目的。編譯器;
代理模式 (Broker pattern)透過解耦元件來構造分散式系統。訊息中介軟體;; 網路傳輸中的代理軟體
點對點模式 (Peer-to-peer pattern)每個元件都稱為對等節點。對等節點既可以作為客戶機(從其他對等節點請求服務),也可以作為伺服器(向其他對等節點提供服務)。檔案共享網路; 多媒體協議;
事件-匯流排模式 (Event-bus pattern)訂閱釋出模式,事件源將訊息釋出到事件匯流排上的特定通道,監聽者訂閱特定的通道。通知服務; 註冊中心;
模型-檢視-控制器模式(Model-view-controller pattern)MVC模式,解耦元件並允許有效的程式碼重用web應用程式架構; GUI 應用程式;
黑板模式 (Blackboard pattern)對於沒有確定解決方案策略的問題非常有用,所有的元件都可以到達黑板。元件可以生成新增到黑板上的新資料物件。元件在黑板上查詢特定型別的資料,並透過與現有的知識源進行模式匹配找到這些資料。語音識別; 車輛識別及追蹤;
直譯器模式 (Interpreter pattern)用於設計一個解釋專用語言編寫的程式元件。資料庫查詢語言,如SQL 用於描述通訊協議的語言;

2.2 瞭解設計模式

在1995年,有四位程式設計界的前輩合著了一本書,書名叫做《Design Patterns: Elements of Reusable Object-Oriented Software》,翻譯過來就是《設計模式:可複用物件導向軟體的基礎》,書裡面總共收錄了23種設計模式。這本書是軟體研發領域重要的里程碑,合著此書的四位作者,被業內稱為GoF(Gang of Four),因此這本書也被人稱為GoF設計模式。

設計模式按照目的來分類有:建立、結構、行為三種,按照作用範圍來分類有:類模式和物件模式兩種。

1.建立型模式:用於建立物件,就是將物件的建立與使用分離。從而降低系統的耦合度,使用者不需要關注物件的建立細節,物件的建立由相關的工廠來完成。

2.結構型模式:描述如何將類,物件,介面之間按某種佈局組成更大的結構。

3.行為型模式:用於描述程式在執行時複雜的流程控制,即描述多個類或物件之間怎樣相互協作共同完成單個物件都無法單獨完成的任務,它涉及演算法與物件間職責的分配。

23種設計模式如下:

型別模式名稱模式描述
建立型單例模式(Singleton)某個類只能生成一個例項,該類提供了一個全域性訪問點供外部獲取該例項,其擴充是有限多例模式。
工廠方法模式(Factory Method)定義一個用於建立產品的介面,由子類決定生產什麼產品。 
抽象工廠模式(AbstractFactory)提供一個建立產品族的介面,其每個子類可以生產一系列相關的產品。 
建造者模式(Builder)將一個複雜物件分解成多個相對簡單的部分,然後根據不同需要分別建立它們,最後構建成該複雜物件。 
原型模式(Prototype)將一個物件作為原型,透過對其進行復制而克隆出多個和原型類似的新例項。 
結構型介面卡模式(Adapter)將一個類的介面轉換成客戶希望的另外一個介面,使得原本由於介面不相容而不能一起工作的那些類能一起工作。
橋接模式(Bridge)將抽象與實現分離,使它們可以獨立變化。它是用組合關係代替繼承關係來實現,從而降低了抽象和實現這兩個可變維度的耦合度。 
組合模式(Composite)將物件組合成樹狀層次結構,使使用者對單個物件和組合物件具有一致的訪問性。 
裝飾模式(Decorator)動態的給物件增加一些職責,即增加其額外的功能。 
外觀模式(Facade)為多個複雜的子系統提供一個一致的介面,使這些子系統更加容易被訪問。 
亨元模式(Flyweight)運用共享技術來有效地支援大量細粒度物件的複用。 
代理模式(Proxy)為某物件提供一種代理以控制對該物件的訪問。即客戶端透過代理間接地訪問該物件,從而限制、增強或修改該物件的一些特性。 
行為型模板方法模式(TemplateMethod)定義一個操作中的演算法骨架,而將演算法的一些步驟延遲到子類中,使得子類可以不改變該演算法結構的情況下重定義該演算法的某些特定步驟。
策略模式(Strategy)定義了一系列演算法,並將每個演算法封裝起來,使它們可以相互替換,且演算法的改變不會影響使用演算法的客戶。 
命令模式(Command)將一個請求封裝為一個物件,使發出請求的責任和執行請求的責任分割開。 
職責鏈模式(Chain of Responsibility)把請求從鏈中的一個物件傳到下一個物件,直到請求被響應為止。透過這種方式去除物件之間的耦合。 
狀態模式(State)允許一個物件在其內部狀態發生改變時改變其行為能力。 
觀察者模式(Observer)多個物件間存在一對多關係,當一個物件發生改變時,把這種改變通知給其他多個物件,從而影響其他物件的行為。 
中介者模式(Mediator)定義一箇中介物件來簡化原有物件之間的互動關係,降低系統中物件間的耦合度,使原有物件之間不必相互瞭解。 
迭代器模式(Iterator)提供一種方法來順序訪問聚合物件中的一系列資料,而不暴露聚合物件的內部表示。 
訪問者模式(Visitor)在不改變集合元素的前提下,為一個集合中的每個元素提供多種訪問方式,即每個元素有多個訪問者物件訪問。 
備忘錄模式(Memento)在不破壞封裝性的前提下,獲取並儲存一個物件的內部狀態,以便以後恢復它。 
直譯器模式(Interpreter)提供如何定義語言的文法,以及對語言句子的解釋方法,即直譯器。 

2.3 小結

•架構模式更像是宏觀戰略層面的設計,設計模式則更像是戰略目標拆解出來的具體任務的實現方案;

•軟體架構是軟體的一種搭建形式,往往規定了軟體的模組組成,通訊介面(含通訊資料結構),元件模型,整合框架等,往往規定了具體的細節;

•設計模式是一種軟體的實現方法,是一種抽象的方法論,是為了更好的實現軟體而歸納出來的有效方法;

•實現一種軟體架構,不同組成部分可能用到不同的設計模式,某個部分也可能可以採用不同的設計模式來實現;

三 應用實踐指南

3.1 適用場景

不使用設計模式也能實現業務訴求,系統也能夠正常執行,為什麼要使用設計模式呢?

是的,相當一部分場景是不需要進行設計模式的引入的,比如:業務邏輯簡單,業務演進方向不明朗,或者就是一個不需要經常迭代的功能點。但當我們遇到了複雜問題設計的時候,就需要藉助前人的經驗了,而設計模式就是前人為我們沉澱總結的各種常見問題的解決方案。

那麼多種設計模式,難道我需要全部系統的學習實現一遍,都要閉著眼睛就能寫出來嗎?其實不用,這就跟排序演算法一樣,我們只需要記住每種演算法的適用範圍和場景就可以了,在有需要的時候,再去深入研究就可以了。以下總結了各種設計模式對應的適用場景:

模式名稱適用場景
單例模式(Singleton)無狀態類使用單例模式可以節省記憶體資源
工廠方法模式(Factory Method)在不知道具體實現細節的情況下建立物件的場景
抽象工廠模式(AbstractFactory)客戶端與物件建立解耦,需要建立多個不同型別的物件的場景
建造者模式(Builder)生成複雜物件的場景
原型模式(Prototype)快速建立大量同類物件的場景
介面卡模式(Adapter)讓兩個不相容的類一起工作的場景
橋接模式(Bridge)將一個類的抽象部分和實現部分獨立改變的場景
組合模式(Composite)表示樹形結構的場景
裝飾模式(Decorator)動態地為物件新增新職責的場景
外觀模式(Facade)為一個複雜的子系統提供一個簡單的介面的場景
亨元模式(Flyweight)在多個地方共享大量細粒度物件的場景
代理模式(Proxy)在訪問某個物件時增加額外控制的場景
模板方法模(TemplateMethod)在不改變演算法結構的情況下重定義演算法中的某些步驟的場景
策略模式(Strategy)在不同情況下使用不同演算法的場景
命令模式(Command)支援命令的撤銷和恢復、延遲呼叫或日誌操作的場景
職責鏈模式(Chain of Responsibility)在不明確指定接收者的情況下,向多個物件中提交一個請求的場景
狀態模式(State)根據物件的狀態來改變它的行為的場景。
觀察者模式(Observer)在物件之間鬆散耦合的場景
中介者模式(Mediator)在多個物件之間鬆散耦合的場景
迭代器模式(Iterator)為容器物件提供多種遍歷方式的場景
訪問者模式(Visitor)在不改變各元素的類的前提下定義對這些元素的新操作的場景
備忘錄模式(Memento)歷史回放或者回滾等場景
直譯器模式(Interpreter)定義一個語言併為該語言實現一個直譯器的場景

3.2 場景案例

為了讓讀者對設計模式有個更加直觀立體的感知,接下來以實際案例為大家展現一下設計模式在實際場景的應用。案例包含了建立型,結構型,行為型各種模式型別裡常用的設計模式,比如:

•用工廠模式隔離業務實現;

•用策略模式消解業務流程分支;

•用模板方法模式提取業務分支公共流程;

•用建造者模式簡化入參物件的構建難度;

•用代理模式橫向擴充套件通用能力(日誌,異常處理);

•用職責鏈模式對請求進行敏感詞,防刷校驗;

•用命令模式讓指令擁有了記憶;

中國有個古諺語:“一個和尚挑水吃,兩個和尚抬水吃,三個和尚等水吃。” 我們就透過程式來模擬出家人的寺廟生活。

工廠模式

首先,這三個人是如何成為和尚的呢?

一號和尚(貧困潦倒型),出生在一個大山裡頭,父母怕他孤單,給他生了5個弟弟,在他9歲那年,恰巧家裡鬧了饑荒,為了吃上飯,進了寺廟,出了家;

二號和尚(走投無路型),出生在一個湖泊旁邊,因為生性耿直,18歲那年,走在街頭,路見不平,三拳打死街上惡霸,為了贖罪,受了戒,墜入空門;

三號和尚(天選之子型),從小敏而好學,性情溫厚,對佛學產生濃厚興趣,13歲那年,為了繼承和光大佛法,斷了塵緣,皈依佛門。

N號和尚,......

每一個和尚的來歷都不盡相同,但在當下喝不上水,這件事情上,都顯得不重要。重要的是,只要湊足三個和尚,就會沒水喝。那麼寺廟如招收和尚?這裡就可以用到工廠模式的思想。

    // 貧困潦倒產生的和尚過程:1.大山裡;2.鬧饑荒;3.要吃飯;
    一號和尚 = HeShangFactoty.getOneHeshang("貧困潦倒型");
    // 走投無路產生的和尚過程:1.生性耿直;2.打死惡霸;3.要贖罪;
    二號和尚 = HeShangFactoty.getOneHeshang("走投無路型");
    // 天選之子產生的和尚過程:1.敏而好學;2.佛學感興趣;3.要廣大佛法;
    三號和尚 = HeShangFactoty.getOneHeshang("天選之子型");

以上示例想體現的是工廠模式能將複雜的物件建立和使用進行了分離設計。下面就以和尚吃水這件事情,用程式的方式詳細展現工廠模式的實現思路。按照和尚的人數,分別有挑,抬,等三種實現方式。以下為基礎程式碼實現:

public interface Waterable {
    Water getWater();
}

public class TiaoShui implements Waterable{
    public Water getWater(){
        System.out.println("先到山下去!");
        return "水是挑上來的!";
    }
} 

public class TaiShui implements Waterable{
    public Water getWater(){
        System.out.println("先到山下去!");
        return "水是抬上來的!";
    }
} 

public class DengShui implements Waterable{
    public Water getWater(){
        System.out.println("就坐在原地!");
        return "水是等不來的!";
    }
} 

具體使用

public class Factory {
    /**
     * 按照和尚數量生成取水物件
     *
     * @param heShangNum 和尚數量
     * @return
     */
    public static Waterable getWaterable(Integer heShangNum) {
        switch (heShangNum) {
            case 1:
                return new TiaoShui();
            case 2:
                return new TaiShui();
            case 3:
                return new DengShui();
            default:
                throw new RuntimeException("廟小,裝不下那麼多和尚!");
        }
    }
}

策略模式

按照不同的條件(人數),分別有幾種獲取水的方法:挑,抬,等。可以透過策略模式來實現,前面的實現方式其實就是策略模式和工廠模式的結合。我們再看一下策略模式的具體使用方式如下:


    /**
     * 透過入參和尚人數,就可以動態改變Waterable.getWater()的取水模式
     * @param heShangNum
     * @return
     */
    public void getWater(Integer heShangNum) {
        Waterable waterable = Factory.getWaterable(heShangNum);
        Water water = waterable.getWater();// 取水
    }

1.輸入引數1:挑水模式的實現(對應Tiaoshui實現類);

2.輸入引數2:抬水模式的實現(對應Taishui實現類);

3.輸入引數3:等不到水模式的實現(對應Dengshui實現類);

透過和尚人數,就可以動態獲得對應的取水實現,即所謂的透過策略實現業務,對於使用方來說(主流程),無需關注取水的具體實現(解耦:業務流程穩定性的設計體現),新增取水方式時,只需要新增一個類實現就可以了,存量的實現和主流程都不會受到影響。

模板方法

我們細化取水過程,取水過程一般需要三步:

1.拿起工具(扁擔或者木棍);

2.到寺廟南面的小河邊(步行);

3.裝滿水帶回寺廟(挑水,抬水,等水);

我們可以將取水流程步驟進行模板化。

public interface Waterable {
    Water getWater();
}

public abstract class AbstractWaterable implements Waterable {
    @Override
    public Water getWater() {
        takeTool();
        toRiver();
        return moveWater();
    }
    /**
     * 拿起工具
     */
    protected abstract String takeTool();

    /**
     * 到河邊去
     */
    protected String toRiver() {
        System.out.println("走過去!");
        return "步行";
    }

    /**
     * 將水帶回來
     *
     * @return
     */
    protected abstract Water moveWater();
}

個性化場景實現

public class TiaoShui extends AbstractWaterable {

    @Override
    protected String takeTool() {
        return "扁擔";
    }

    @Override
    protected Water moveWater() {
        return "挑水";
    }
}

public class Taishui extends AbstractWaterable{
    @Override
    protected String takeTool() {
        return "木棍";
    }

    @Override
    protected Water moveWater() {
        return "抬水";
    }
}

public class DengShui extends AbstractWaterable{
    @Override
    protected String takeTool() {
        return "意念";
    }

    @Override
    protected String toRiver() {
        return "一動不動";
    }

    @Override
    protected Water moveWater() {
        return "無水";
    }
}

具體使用

    /**
     * 和尚取水:實現一個和尚挑水喝,兩個和尚抬水喝,三個和尚等水喝
     */
    public void fetchWater(){
        // 
        for (int heShangNum = 1; heShangNum < 4; heShangNum++) {
            Waterable waterable = Factory.getWaterable(heShangNum);
            Water water = waterable.getWater();
        }
    }

模板方法講的是流程標準定義和能力複用。示例中,定義了取水的三個階段,選擇工具,出行方式,搬運方式。單看出行方式中,【挑水】和【抬水】複用了模板方法裡的通用實現,【等水】則個性化的重寫了出行方式。

建造者模式

我們取水需要一些工具,按照取水方式(挑,抬,等)可以分為扁擔+木桶,木棍+木桶,意念(什麼也不需要)等裝備的組合方式。如何定義getWater(ToolBox toolBox)的入參ToolBox,使其能夠按照對應取水方式匹配正確的裝備組合呢?這裡就可以使用建造者模式。

public class ToolBox {
    private final String bianDan;
    private final String muTong;
    private final String muGun;

    private ToolBox(TiaoBuilder builder){
        this.bianDan=builder.bianDan;
        this.muTong=builder.muTong;
        this.muGun = null;
    }
    private ToolBox(TaiBuilder builder){
        this.bianDan = null;
        this.muTong = null;
        this.muGun=builder.muGun;
    }
    private ToolBox(DengBuilder builder){
        this.bianDan = null;
        this.muTong = null;
        this.muGun=null;
    }
    public static class TiaoBuilder{
        private String bianDan;
        private String muTong;

        public TiaoBuilder setBianDan(String bianDan) {
            this.bianDan = bianDan;
            return this;
        }
        public TiaoBuilder setMuTong(String muTong) {
            this.muTong = muTong;
            return this;
        }
        public ToolBox build(){
            return new ToolBox(this);
        }
    }

    public static class TaiBuilder{
        private String muGun;
        private String muTong;

        public TaiBuilder setMuGun(String muGun) {
            this.muGun = muGun;
            return this;
        }
        public TaiBuilder setMuTong(String muTong) {
            this.muTong = muTong;
            return this;
        }
        public ToolBox build(){
            return new ToolBox(this);
        }
    }

    public static class DengBuilder{
        public ToolBox build(){
            return new ToolBox(this);
        }
    }

    //省略getter方法
}

具體使用

ToolBox oneHeShangToolBox = new ToolBox.TiaoBuilder().setMuTong("小號木桶").setBianDan("小號扁擔").build();
ToolBox twoHeShangToolBox = new ToolBox.TaiBuilder().setMuTong("大號木桶").setMuGun("長號木棍").build();
ToolBox threeHeShangToolBox = new ToolBox.DengBuilder().build();

建造者模式屬於建立型設計模式,它可以將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。

代理模式

為了鼓勵多勞多得,廟裡取水開始採用積分機制,每取回來一桶水就要敲一下木魚,打一次卡。這裡就可以採用代理模式。代理分為靜態代理和動態代理,為了簡單起見,這裡就用靜態代理來舉例。

    public class WaterableProxy implements Waterable{
    /**
     * 被代理的原始物件
     */
    private Waterable waterable;
    
    public WaterableProxy(Waterable waterable) {
        this.waterable = waterable;
    }

    @Override
    public Water getWater() {
        Water water = waterable.getWater();
        // 增強的新功能,不管是挑水,抬水,等水,只有帶回來水,就可以
        if(water != "無水"){
            System.out.println("我敲一下木魚,打一次卡!");
        }
        return water;
    }
}

具體使用

    public void doProxy(){
        Waterable waterable = new Taishui();
        WaterableProxy proxyWaterable = new WaterableProxy(waterable);
        proxyWaterable.getWater();
    }

代理模式就是代理物件具備真實物件的功能,並代替真實物件完成相應操作,並能夠在操作執行的前後,對操作進行增強處理。(透過代理訪問真實物件)

責任鏈模式

為了升級取水工具,將小木桶升級大金桶,寺廟決定對外提供燒香拜佛,誦經禮佛等增值服務。為了安全起見,寺廟引進了安檢機制,流程是這樣的:

•禁止攜帶寵物;

•衣著穿戴整齊;

•其它業障,最終解釋權歸寺廟;

public interface SecureFilter {
    void filter(Map context);
}

public class PetSecure implements SecureFilter{
    @Override
    public void filter(Map context) {
        if(context.containsKey("寵物")){
            throw new RuntimeException("請出去:禁止攜帶寵物進入!");
        }
    }
}

public class WearSecure implements SecureFilter{
    @Override
    public void filter(Map context) {
        if(context.containsKey("光膀子")){
            throw new RuntimeException("請出去:有傷風化者!");
        }
    }
}

public class OtherSecure implements SecureFilter{
    @Override
    public void filter(Map context) {
        if(context.containsKey("大聲喧譁")){
            throw new RuntimeException("請出去:佛門乃清淨之地!");
        }
    }
}

具體使用

/**
 * 安檢責任鏈實現
 */
class SecureChain implements SecureFilter{
    // 注入PetSecure,WearSecure,OtherSecure等過濾器
    private List<SecureFilter> secureFilterList;
    /**
     * 進入寺廟,進行安檢邏輯
     * @param context
     */
    @Override
    public void filter(Map context) {
        // 進行安檢流程
        for (SecureFilter secureFilter : secureFilterList) {
            secureFilter.filter(context);
        }
        System.out.println("佛祖保佑,安檢透過!");
    }
}

流程示意圖

責任鏈模式一般和過濾器模式組合一起使用,即建立一個鏈條,經過這個鏈條處理的所有物件和資料分別進行依次加工,每個環節負責處理不同的業務,環節間彼此獨立解耦,同時可以複用。這種設計的巧妙之處在於可以鏈式呼叫,不同的過濾方式可以靈活的排序和組合。既可以使用單個過濾器進行處理,也可以直接新增一條責任鏈。

命令模式

寺廟裡的和尚除了打水工作之外,還有很多工作要做,所有的工作安排都是按照主持的指令來執行的,比如某日清晨的工作安排如下:

1.一號和尚做早餐;

2.二號和尚掃庭院;

3.三號和尚敲古鐘;

結構定義

public class Command implements Serializable {
    // 做早餐,打掃,敲鐘等指令標識
    private OrderTypeEnum order;
    // 正向執行OR逆向回滾
    private Integer direction;
    // 省略get和set方法
}

// 指令動作執行器,每種指令對應一個實現
public interface OrderHandler {
 /**
     * 執行邏輯
     *
     * @param callContext
     * @return
     */
    PipeResult execute(CallContext callContext);
    /**
     * 支援的命令型別:做早餐,打掃,敲鐘等命令標識
     *
     * @return
     */
    OrderTypeEnum getOrderType();

}

// 指令型別管理器
public interface PipelineCmd {

    /**
     * 指令行定義
     *
     * @return
     */
    Command getCommand();

    /**
     * 執行邏輯
     *
     * @param pipeContext
     * @return
     */
    PipeResult execute(PipeContext pipeContext);

    /**
     * 如果可以撤消指令,則此方法應返回true ,否則返回false
     *
     * @return
     */
    default boolean isReversible() {
        return true;
    }
}
 
 // 指令執行器管理器
 public interface CmdHandler {
    /**
     * 業務執行
     *
     * @param callContext
     * @return
     */
    PipeResult execute(CallContext callContext);

    /**
     * 業務回滾(只回滾當前指令)
     *
     * @param callContext
     * @return
     */
    PipeResult rollback(CallContext callContext);

    /**
     * 全部回滾
     *
     * @param pipeContext
     * @return
     */
    PipeResult rollbackAll(PipeContext pipeContext);
}

命令實現

public class ZhuChiCmd implements PipelineCmd {
    private Command command;
    private transient OrderHandler orderHandler;

    public StepCmd(Command command, OrderHandler orderHandler) {
        this.command = command;
        this.orderHandler= orderHandler;
    }

    @Override
    public PipeResult execute(PipeContext pipeContext) {
        return orderHandler.execute(new CallContext(command, pipeContext));
    }
    // 省略get和set方法
}
    
    
public class Breakfast implements OrderHandler {
 /**
     * 執行邏輯
     *
     * @param callContext
     * @return
     */
    PipeResult execute(CallContext callContext){
        System.out.println("做早餐啦!");
    }
    /**
     * 支援的指令型別:做早餐,打掃,敲鐘等指令標識
     *
     * @return
     */
    OrderTypeEnum getOrderType(){
        return OrderTypeEnum.BREAKFAST;
    }

}

public class Clean implements OrderHandler {
 /**
     * 執行邏輯
     *
     * @param callContext
     * @return
     */
    PipeResult execute(CallContext callContext){
        System.out.println("打掃庭院啦!");
    }
    /**
     * 支援的指令型別:做早餐,打掃,敲鐘等命令標識
     *
     * @return
     */
    OrderTypeEnum getOrderType(){
        return OrderTypeEnum.CLEAN;
    }

}

public class Ring implements OrderHandler {
 /**
     * 執行邏輯
     *
     * @param callContext
     * @return
     */
    PipeResult execute(CallContext callContext){
        System.out.println("敲鐘啦!");
    }
    /**
     * 支援的命令型別:做早餐,打掃,敲鐘等指令標識
     *
     * @return
     */
    OrderTypeEnum getOrderType(){
        return OrderTypeEnum.Ring;
    }

}

public class CmdFactory {
    private List<OrderHandler> orderHandlerList;

    /**
     * 獲取指定指令條件的指令物件
     *
     * @param command
     * @return
     */
     public PipelineCmd getPipelineCmd(Command command) {
        for (OrderHandler orderHandler : orderHandlerList) {
            OrderTypeEnum orderTypeEnum = orderHandler.getOrderType();
            if (orderTypeEnum.equals(command.getOrder())) {
                return new ZhuChiCmd(command, orderHandler);
            }
        }
        throw new RuntimeException("對不起主持:沒有多餘的和尚來執行新命令了!");
    }
     /**
     * 獲取給定指令的回滾操作指令物件
     *
     * @param command
     * @return
     */
    public PipelineCmd getRollbackPipelineCmd(Command command) {
        Command rollbackCommand = getRollbackCommand(command);
        return getPipelineCmd(rollbackCommand);
    }
}

具體使用

public class CmdHandlerImpl implements CmdHandler {
    private CmdFactory cmdFactory;

    @Override
    public PipeResult execute(CallContext callContext) {
        PipelineCmd pipelineCmd = cmdFactory.getPipelineCmd(callContext.getCommand());
        PipeResult pipeResult = pipelineCmd.execute(callContext.getPipeContext());
        return pipeResult;
    }

    @Override
    public PipeResult rollback(CallContext callContext) {
        Command rollbackCommand = cmdFactory.getRollbackCommand(callContext.getCommand());
        if (rollbackCommand == null) {
            return new PipeResult("不需要回滾");
        }
        PipelineCmd pipelineCmd = cmdFactory.getPipelineCmd(rollbackCommand);
        if (!pipelineCmd.isReversible()) {
            return new PipeResult("不支援回滾");
        }
        PipeResult pipeResult = pipelineCmd.execute(callContext.getPipeContext());
        return pipeResult;
    }

    @Override
    public PipeResult rollbackAll(PipeContext pipeContext) {
        // 命令執行備忘錄模式物件,這裡不再展開
        Caretaker<Command> caretaker = pipeContext.getCaretaker();
        // 拿到上一步執行命令,依次迴圈回滾
       Command command = caretaker.pop();
        while (command != null) {
            PipelineCmd pipelineCmd = cmdFactory.getRollbackPipelineCmd(command);
            if (pipelineCmd != null) {
                pipelineCmd.execute(pipeContext);
            }
            command = caretaker.pop();
        }
        return new PipeResult();
    }

}

命令模式將一個請求封裝為一個物件,使發出的請求的物件和執行請求的物件分割開。這兩者之間透過命令物件進行溝通,這樣方便將命令物件進行儲存、傳遞、呼叫、增加與管理。命令模式可以與備忘錄模式組合使用,方便實現Undo和Redo操作。

3.3 實踐心得

設計原則

具體包含單一職責原則SRP、開閉原則OCP、里氏替換原則LSP、依賴倒置原則DIP、介面隔離原則ISP、最少知識原則LKP等很多種,其核心還是圍繞著低耦合,高複用,高內聚,易擴充套件,易維護展開的。

模式與原則

1.設計原則是指導思想,設計模式是實現手段之一;

2.設計原則在實際開發中並不能做到完全遵守,往往是打破一些原則,遵守一些原則,來實現設計的合理性;(成本,效能)

3.設計模式往往是問題解決方案的骨架,有時候可以當做開發規範和任務拆分執行落地的技術手段;

4.一個設計模式,往往不僅僅採用一種設計原則,而是一些設計原則的整合;

5.設計模式不是一成不變的,可以根據問題場景,輸出新的模式;

6.一個複雜場景問題,有時候需要多種設計模式的組合;

7.學設計模式,死記硬背是沒用的,要從實踐中習得;

8.避免設計過度,使簡單的問題複雜化。一定要牢記簡潔原則,設計模式是為了使設計簡單,而不是更復雜;

四 總結

本文從設計模式與程式語言的關係,設計模式與架構模式的區別,設計原則和設計模式的關係等幾個維度進行了分析和解答。關於設計模式應該如何學習和應用的問題,給出了學習意見和實踐心得。當然,為了讓設計模式更加的直觀和立體,也花了大量篇幅在應用實踐案例上面,主要是透過場景化的案例,以設計模式的方式給出解決方案,其中部分場景為了方便理解,將問題做了簡化處理,但這不影響我們去理解設計模式要解決的問題型別。冰凍三尺非一日之寒,滴水石穿非一日之功,希望本文能夠為你帶來幫助。

相關文章