架構師日記-深入理解軟體設計模式

来源:https://www.cnblogs.com/Jcloud/archive/2023/05/05/17374349.html
-Advertisement-
Play Games

本文從設計模式與編程語言的關係,設計模式與架構模式的區別,設計原則和設計模式的關係等幾個維度進行了分析和解答。關於設計模式應該如何學習和應用的問題,給出了學習意見和實踐心得。 ...


作者:京東零售 劉慧卿

一 設計模式與編程語言

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.避免設計過度,使簡單的問題複雜化。一定要牢記簡潔原則,設計模式是為了使設計簡單,而不是更複雜;

四 總結

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 Symbol是JavaScript中的原始數據類型之一,它表示一個唯一的、不可變的值,通常用作對象屬性的鍵值。由於Symbol值是唯一的,因此可以防止對象屬性被意外地覆蓋或修改。以下是Symbol的方法和屬性整理: 屬性 Symbol.l ...
  • 前言 之前寫了一個vue+django的一個通過串口控制的上位機系統。但是實際生產中,不如部署到伺服器上,這樣可以更好的節約成本。但是這樣就需要弄一個客戶端來控制處理串口信息。那我就在想能不能通過網頁直接拿到客戶端的串口信息。所以問了萬能的chatgpt,得到了以下答案: 是的,前端可以使用 Web ...
  • 在 HTML5 中,文檔對象(即 document 對象)具有一個 visibilityState 屬性,該屬性表示當前文檔對象的可見性狀態。 visibilityState 可能的取值有以下三種: - visible :表示文檔當前處於激活狀態,即當前選項卡處於前臺或當前視窗處於屏幕最上層。- h ...
  • 在隨筆《基於SqlSugar的開發框架循序漸進介紹(28)-- 快速構建系統參數管理界面》中介紹了基於SqlSugar開發框架,構建系統參數管理的後端API部分,以及WInform界面部分內容,本篇隨筆介紹基於Vue3+ElementPlus的前端界面開發過程。 ...
  • 此文為系列文章第一篇,為淺嘗輒止的引入,目的是為了讓前端從業人員及非從業但是對此領域感興趣的人對於”前端“是乾什麼的這個話題有個無門檻的瞭解。 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 1. 原型鏈 溫故而知新: 構造函數、原型和實例的關係: 每個構造函數都有一個原型對象,原型有一個屬性指回構造函數,實例有一個內部指針指向原型。 思考:如果原型是另一個類型的實例呢? 那就意味著這個原型本身有一個內部指針指向另一個原型,相 ...
  • 在HTML中, link 標簽是一個自閉合元素,通常位於文檔的 head 部分。它用於建立與外部資源的關聯,如樣式表、圖標等。 link 標簽具有多個屬性,其中 rel 和 href 是最常用的。 rel 屬性定義了當前文檔與鏈接資源之間的關係。常見的 rel 屬性值有: - stylesheet ...
  • 某次遇到一個從0到1的大型項目,該項目涉及兩個端,除了鑒權和部分業務邏輯不同外,頁面UI和其餘邏輯幾乎一致,遇到這種項目,該如何架構?既能保證項目順利開發完成,又能保證後期的迭代、維護、可擴展? ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...