再談23種設計模式(3):行為型模式(學習筆記)

zhoulujun發表於2024-06-06

23 種設計模式的分類表

範圍\目的建立型模式結構型模式行為型模式
類模式 工廠方法 (類)介面卡 模板方法、直譯器
物件模式 單例
原型
抽象工廠
建造者

代理

裝飾

橋接

(物件)介面卡
外觀
享元
組合
策略
職責鏈
狀態
觀察者
中介者
迭代器
訪問者

備忘錄

命令

結構型模式VS行為型模式

  • 結構型模式則關注於如何組合類和物件以形成更大的結構,以及如何簡化這些結構(描述系統各要素之間的關係)。它們透過繼承或組合來組織類或物件,以實現更大的功能。結構型模式幫助確保當一個部分改變時,整個系統的結構不需要做出大的改變。這些模式主要關注於組合物件以形成新的結構,以便更好地管理複雜性——其是,結構模型就是一種集合模型,節點表示系統要素,有向邊表示要素之間的關係。

  • 行為型模式的關注點在於物件之間的通訊和職責分配(描述結構模型中物件的動態特徵)。它們主要用於管理物件之間的演算法、關係和職責。行為型模式涉及到類和物件如何互動以及分配職責。這些模式通常描述了物件之間的通訊方式,以及如何協作完成任務。

總結一下,區別在於:

  • 結構型模式關注的是物件和類的組織,即它們是如何構成更大的結構的,以及如何透過這種方式來簡化系統的設計或提高系統的靈活性。

  • 行為型模式關注的是物件之間的交雲和協作,即它們是如何相互作用的,以及如何分配職責和演算法來完成任務。

行為模型可以透過各種圖形表示,如STD-狀態轉換圖、活動圖、狀態機圖等。

行為型模式(Behavioral Design Patterns)概述:

  • 模板模式(Template pattern):定義一個演算法結構,而將一些步驟延遲到子類實現。

  • 直譯器模式(Interpreter pattern):給定一個語言,定義它的文法的一種表示,並定義一個直譯器。

  • 策略模式(strategy pattern):定義一系列演算法,把他們封裝起來,並且使它們可以相互替換。

  • 狀態模式(State pattern):允許一個物件在其物件內部狀態改變時改變它的行為。

  • 觀察者模式(observer pattern):物件間的一對多的依賴關係。

  • 備忘錄模式(Memento pattern):在不破壞封裝的前提下,保持物件的內部狀態。

  • 中介者模式(Mediator pattern):用一箇中介物件來封裝一系列的物件互動。

  • 命令模式(Command pattern):將命令請求封裝為一個物件,使得可以用不同的請求來進行引數化。

  • 訪問者模式(visitor pattern):在不改變資料結構的前提下,增加作用於一組物件元素的新功能。

  • 責任鏈模式(Chain of responsibility pattern):將請求的傳送者和接收者解耦,使的多個物件都有處理這個請求的機會。

  • 迭代器模式(iterator pattern):一種遍歷訪問聚合物件中各個元素的方法,不暴露該物件的內部結構。

模板方法模式/模板模式 (Template Method Pattern)

定義一個操作的演算法骨架,允許子類重新定義某些步驟而不改變演算法的結構。

在抽象類中公開定義了執行的方法,子類可以按需重寫其方法,但是要以抽象類中定義的方式呼叫方法。總結起來就是:定義一個操作的演算法結構,而將一些步驟延遲到子類中。在不改變演算法結構的情況下,子類能重定義該演算法的特定步驟。

無論造型如何變化,不變的有兩種東西:“奶油”和“麵包”。其餘的材料隨意搭配,就湊成了各式各樣的蛋糕。

模板方法模板方法整理相似點

模板方法模式其實是一個比較簡單的設計模式,它有如下優點:

  1. 封裝不變的邏輯,擴充套件差異化的邏輯;

  2. 抽取公共程式碼,提高程式碼的複用性;

  3. 父類控制行為,子類實現細節

模板模式結構

  1. 模板類(Abstract Class): 定義了一個演算法的骨架,其中包含一個或多個抽象方法,這些方法由子類實現。還可以包含一些具體方法,這些方法在演算法中的步驟中被使用,但它們的實現可以在子類中被重寫。

  2. 具體子類(Concrete Class): 實現了在模板類中定義的抽象方法,完成了演算法的特定步驟。

模板模式優缺點

優點

  • 抽取公共程式碼,程式碼複用,減少重複程式碼。

  • 提高程式碼的可維護性,因為演算法結構在模板類中統一定義,易於理解和修改。

  • 提供了一個穩定的演算法框架,方便框架擴充套件。

缺點

  • 模板方法模式可能會導致類的數目增加,因為每個具體子類對應一個具體實現。

  • 可能會在一定程度上限制了某些子類的靈活性,因為部分演算法結構在父類中已經確定。

更詳細的瞭解,推薦閱讀:

  • 聊一聊模板方法模式 https://zhuanlan.zhihu.com/p/629320399

  • 什麼是模板方法模式? - 大橘為重的回答 - 知乎 https://www.zhihu.com/question/573793216/answer/3330820558

  • 什麼是模板方法模式? - Lion Long的回答 - 知乎 https://www.zhihu.com/question/573793216/answer/3255332354

典型場景:

  1. 演算法骨架不變,但某些步驟的具體實現可能會變化: 當有一個演算法需要在其框架內保持不變的結構,但某些步驟的具體實現可能會隨著演算法的不同變化時,可以使用模板方法模式。

  2. 程式碼複用: 當有多個類有相似的演算法,並且希望透過共享相同的程式碼來減少重複時,可以使用模板方法模式。

  3. 邏輯結構清晰,固定演算法流程: 當演算法的執行流程在各個子類中基本相同,只有某些細節不同的情況下,模板方法模式可以避免程式碼重複,使得結構更清晰。 模板方法模式定義了一個清晰的演算法骨架,使得整個演算法的邏輯結構更加明確。這有助於開發人員理解和維護程式碼。

  4. 框架設計: 在框架設計中,通常定義了一些抽象的模板類,由子類實現具體的演算法細節。這樣可以提供框架的穩定結構,同時允許具體的實現在子類中靈活變化。

模板方法模式和抽象工廠模式

  • 模板方法模式屬於行為型模式,它定義了一個操作中的演算法的骨架,並將一些步驟延遲到子類中實現。這樣,演算法的結構可以在不改變演算法的情況下重新定義演算法的某些特定步驟。模板方法模式在一個方法中定義了一個演算法的步驟,並允許子類為一個或多個步驟提供實現。這樣,子類可以在不改變演算法結構的前提下,重新定義演算法的某些特定步驟。

  • 抽象工廠模式屬於建立型模式,它提供了一個介面,用於建立一系列相關或相互依賴的物件,而不需要指定它們具體的類。抽象工廠允許客戶端使用抽象的介面來建立一組相關的產品,而不需要關心這些產品的具體實現。這樣可以在不影響客戶端的情況下更換產品族或產品的具體實現。

模板方法模式與抽象工廠模式主要區別在於:
  • 目的不同

    • 模板方法模式的目的是定義一個演算法的骨架,並允許子類為演算法的某些步驟提供具體實現。

    • 抽象工廠模式的目的是建立一系列相關或依賴的物件,而不需要關心這些物件的具體類。

  • 設計層次不同:

    • 模板方法模式主要關注行為和演算法的封裝,透過繼承機制來實現。

    • 抽象工廠模式關注物件建立和組合的封裝,透過物件組合來實現。

  • 應用場景不同:

    • 模板方法模式通常用於需要定義演算法步驟的場景,允許子類在不改變演算法結構的情況下,改變或擴充套件演算法中的某些步驟。

    • 抽象工廠模式通常用於系統需要獨立於產品的建立、組合和表示時,系統配置有多個產品族,而系統只消費其中某一族的產品

  • 應用領域不同:

    • 模板方法模式常用在框架的核心,如Spring的JDBC Template、Hibernate的Session Factory等。

    • 抽象工廠模式更多地出現在庫或工具包中,比如Apache Commons Logging、SLF4j等日誌記錄庫。

下面是模板方法模式

// 抽象類
class Cake {
    // 模板方法,定義了做蛋糕的步驟
    makeCake() {
        this.prepareIngredients();
        this.bake();
        this.decorate();
    }

    // 以下方法由子類實現
    prepareIngredients() {
        throw new Error("prepareIngredients() must be overridden");
    }

    bake() {
        console.log("Baking the cake. Be patient.");
    }

    decorate() {
        throw new Error("decorate() must be overridden");
    }
}

// 具體類 - 草莓蛋糕
class StrawberryCake extends Cake {
    prepareIngredients() {
        console.log("Gathering ingredients for a Strawberry Cake.");
    }

    decorate() {
        console.log("Decorating the cake with strawberries.");
    }
}

// 具體類 - 芒果蛋糕
class MangoCake extends Cake {
    prepareIngredients() {
        console.log("Gathering ingredients for a Mango Cake.");
    }

    decorate() {
        console.log("Decorating the cake with mango slices.");
    }
}

// 使用模板方法
const strawberryCake = new StrawberryCake();
strawberryCake.makeCake();

const mangoCake = new MangoCake();
mangoCake.makeCake();

接下來,我們來實現抽象工廠模式的例子。我們將建立一個抽象工廠 CakeFactory,它定義了建立蛋糕和裝飾品的介面。然後,我們將建立兩個具體的工廠 StrawberryCakeFactory 和 MangoCakeFactory,它們分別實現了 CakeFactory 類中定義的方法。

// 抽象工廠
class CakeFactory {
    createCake() {
        throw new Error("createCake() must be overridden");
    }

    createDecoration() {
        throw new Error("createDecoration() must be overridden");
    }
}

// 具體工廠 - 草莓蛋糕工廠
class StrawberryCakeFactory extends CakeFactory {
    createCake() {
        return new StrawberryCake();
    }

    createDecoration() {
        return "Strawberries";
    }
}

// 具體工廠 - 芒果蛋糕工廠
class MangoCakeFactory extends CakeFactory {
    createCake() {
        return new MangoCake();
    }

    createDecoration() {
        return "Mango slices";
    }
}

// 客戶端程式碼
function makeCake(factory) {
    const cake = factory.createCake();
    const decoration = factory.createDecoration();
    cake.makeCake();
    console.log(`Decorating with: ${decoration}`);
}

// 使用抽象工廠
const strawberryFactory = new StrawberryCakeFactory();
makeCake(strawberryFactory);

const mangoFactory = new MangoCakeFactory();
makeCake(mangoFactory);

上面兩段程式碼展示了模板方法模式和抽象工廠模式的不同應用場景和核心點。讓我們來分析它們的主要差異:

模板方法模式的核心點:

  • 定義了一個演算法的骨架(makeCake 方法),並允許子類(StrawberryCake 和 MangoCake)在不改變演算法結構的情況下,重寫演算法的某些特定步驟(prepareIngredients 和 decorate 方法)。

  • 透過繼承來實現程式碼複用和擴充套件功能。子類繼承了父類 Cake 並提供了特定步驟的具體實現。

  • 客戶端直接與具體的子類例項互動,子類例項控制了演算法的某些特定步驟。

抽象工廠模式的核心點:

  • 提供了一個介面(CakeFactory)用於建立一系列相關或依賴的物件(createCake 和 createDecoration 方法),而不需要指定它們具體的類。

  • 透過物件組合來實現功能的擴充套件。具體工廠(StrawberryCakeFactory 和 MangoCakeFactory)實現了工廠介面並建立了具體的產品。

  • 客戶端程式碼(makeCake 函式)與抽象工廠介面互動,而不是直接與具體產品交雲。這允許在不改變客戶端程式碼的情況下更換工廠和產品。

策略模式 (Strategy Pattern)

定義一系列演算法,並使它們可以互換——該模式定義了一系列演算法,並將每個演算法封裝起來,使它們可以相互替換,且演算法的變化不會影響使用演算法的客戶,即:策略模式允許演算法獨立於使用它們的客戶端而變化。

旅遊選擇出行模式有很多種開發需要選擇一款開發工具

典型場景:

  • 當需要在執行時更改演算法時。例如,在排序演算法中,可以使用策略模式來選擇不同的排序演算法。

  • 導航軟體中的路線規劃,可以根據使用者的選擇(最快、最短、避開收費路段)來選擇不同的演算法。

策略模式屬於物件行為模式,它透過對演算法進行封裝,把使用演算法的責任和演算法的實現分割開來,並委派給不同的物件對這些演算法進行管理。

策略模式結構

策略模式的主要角色如下:

  • 抽象策略(Strategy)類:這是一個抽象角色,通常由一個介面或抽象類實現。此角色給出所有的具體策略類所需的介面。

  • 具體策略(Concrete Strategy)類:實現了抽象策略定義的介面,提供具體的演算法實現或行為。

  • 環境(Context)類:持有一個策略類的引用,最終給客戶端呼叫。

策略模式程式碼示例:

// 策略物件定義不同的驗證演算法
const validationStrategies = {
    isNonEmpty: {
        validate: (value) => value !== '',
        message: 'The value cannot be empty'
    },
    isNumber: {
        validate: (value) => !isNaN(parseFloat(value)) && isFinite(value),
        message: 'The value must be a number'
    },
    isEmail: {
        validate: (value) => /\S+@\S+\.\S+/.test(value),
        message: 'The value must be an email address'
    }
};

// Context 類,使用策略物件的 validate 方法
class Validator {
    constructor() {
        this.errors = [];
        this.validationStrategies = {};
    }
    // 新增驗證規則
    addValidation(field, strategy) {
        this.validationStrategies[field] = strategy;
    }
    // 執行驗證
    validate(data) {
        this.errors = [];
        for (const field in this.validationStrategies) {
            const strategy = this.validationStrategies[field];
            if (!strategy.validate(data[field])) {
                this.errors.push({ field: field, error: strategy.message });
            }
        }
        return this.errors.length === 0;
    }

    // 獲取驗證錯誤資訊
    getErrors() {
        return this.errors;
    }
}

// 使用策略模式的客戶端程式碼
const validator = new Validator();
validator.addValidation('username', validationStrategies.isNonEmpty);
validator.addValidation('age', validationStrategies.isNumber);
validator.addValidation('email', validationStrategies.isEmail);

const data = {
    username: 'JohnDoe',
    age: '30',
    email: 'johndoe@example.com'
};

const isValid = validator.validate(data);
if (isValid) {
    console.log('Validation passed');
} else {
    console.error('Validation failed:', validator.getErrors());
}

下面的程式碼改為typescript(純種的策略模式結構)

// 抽象策略(Strategy)介面
interface ValidationStrategy {
    validate(value: string): boolean;
    errorMessage: string;
}

// 具體策略(Concrete Strategy)類 - 非空驗證
class NonEmptyValidation implements ValidationStrategy {
    errorMessage = 'The value cannot be empty';

    validate(value: string): boolean {
        return value !== '';
    }
}

// 具體策略(Concrete Strategy)類 - 數字驗證
class NumberValidation implements ValidationStrategy {
    errorMessage = 'The value must be a number';

    validate(value: string): boolean {
        return !isNaN(parseFloat(value)) && isFinite(value as any);
    }
}

// 具體策略(Concrete Strategy)類 - 郵箱驗證
class EmailValidation implements ValidationStrategy {
    errorMessage = 'The value must be an email address';

    validate(value: string): boolean {
        return /\S+@\S+\.\S+/.test(value);
    }
}

// 環境(Context)類
class Validator {
    private errors: string[] = [];
    private validationStrategies: Map<string, ValidationStrategy> = new Map();

    // 新增驗證規則
    addValidation(field: string, strategy: ValidationStrategy): void {
        this.validationStrategies.set(field, strategy);
    }

    // 執行驗證
    validate(data: { [key: string]: string }): boolean {
        this.errors = [];
        for (const [field, strategy] of this.validationStrategies) {
            if (!strategy.validate(data[field])) {
                this.errors.push(`${field}: ${strategy.errorMessage}`);
            }
        }
        return this.errors.length === 0;
    }

    // 獲取驗證錯誤資訊
    getErrors(): string[] {
        return this.errors;
    }
}

// 客戶端程式碼
const validator = new Validator();
validator.addValidation('username', new NonEmptyValidation());
validator.addValidation('age', new NumberValidation());
validator.addValidation('email', new EmailValidation());

const data = {
    username: 'JohnDoe',
    age: '30',
    email: 'johndoe@example.com'
};

const isValid = validator.validate(data);
if (isValid) {
    console.log('Validation passed');
} else {
    console.error('Validation failed:', validator.getErrors());
}

具體推薦閱讀

設計模式(十八)----行為型模式之策略模式 https://www.cnblogs.com/xiaoyh/p/16560074.html

觀察者模式 (Observer Pattern)

定義物件之間的一對多依賴關係,以便當一個物件的狀態發生改變時,所有依賴它的物件都會得到通知並自動更新。

釋出訂閱模式,微商類比

典型場景:

當需要鬆散耦合物件並允許它們在不瞭解彼此的情況下進行通訊時。例如,

  • 在 GUI 中,當使用者更改文字框中的文字時,觀察者模式可以通知所有依賴該文字框的控制元件。

  • 新聞訂閱服務,當新聞更新時,所有訂閱者都會收到通知。

具體推薦閱讀

  • 觀察者模式與釋出訂閱模式的區別https://www.zhoulujun.cn/html/theory/engineering/model/9072.html

迭代器模式 (Iterator Pattern)

提供一種方法來順序訪問集合中的元素,而不暴露集合的底層表示。

迭代器模式STL--迭代器設計原則和萃取機制(Traits) - jcxioo - 部落格園

典型場景:

當需要遍歷集合中的元素,但又不想直接訪問集合的內部結構時。例如

  • 程式語言直譯器或表示式計算器,解釋特定的語法或表示式。

  • 在 Java 中,迭代器模式用於遍歷集合中的元素。

  • jQuery 的 $.each() 函式也是一個迭代器。它可以遍歷任何型別的 jQuery 物件,包括 DOM 元素、HTML 字串、JSON 物件甚至是普通的 JavaScript 物件。

迭代器模式結構

迭代器模式主要包含以下角色:

  • 抽象聚合(Aggregate)角色:定義儲存、新增、刪除聚合元素以及建立迭代器物件的介面。

  • 具體聚合(ConcreteAggregate)角色:實現抽象聚合類,返回一個具體迭代器的例項。

  • 抽象迭代器(Iterator)角色:定義訪問和遍歷聚合元素的介面,通常包含 hasNext()、next() 等方法。

  • 具體迭代器(Concretelterator)角色:實現抽象迭代器介面中所定義的方法,完成對聚合物件的遍歷,記錄遍歷的當前位置。

迭代器的程式碼實現:

// 定義一個迭代器介面
interface Iterator<T> {
  hasNext(): boolean;
  next(): T;
}

// 定義一個具體迭代器
class ConcreteIterator<T> implements Iterator<T> {
  private collection: T[];
  private index: number = 0;

  constructor(collection: T[]) {
    this.collection = collection;
  }

  hasNext(): boolean {
    return this.index < this.collection.length;
  }

  next(): T {
    return this.collection[this.index++];
  }
}

// 定義一個聚合介面
interface Aggregate<T> {
  createIterator(): Iterator<T>;
}

// 定義一個具體聚合
class ConcreteAggregate<T> implements Aggregate<T> {
  private collection: T[];

  constructor(collection: T[]) {
    this.collection = collection;
  }

  createIterator(): Iterator<T> {
    return new ConcreteIterator(this.collection);
  }
}

// 使用迭代器模式
const aggregate = new ConcreteAggregate([1, 2, 3, 4, 5]);
const iterator = aggregate.createIterator();

while (iterator.hasNext()) {
  console.log(iterator.next());
}

具體參看:

設計模式(二十四)----行為型模式之迭代器模式 https://www.cnblogs.com/xiaoyh/p/16563331.html

責任鏈模式 (Chain of Responsibility Pattern)

將請求鏈式傳遞給一系列處理程式,直到有一個處理程式處理該請求。

責任鏈模式很好理解簡單實現責任鏈模式

典型場景:

當需要將請求處理委託給多個物件,並且處理順序不重要時。例如

  • 客戶支援系統,請求可以在多個處理者之間傳遞,直到找到合適的處理者。

  • 在 Web 伺服器中,責任鏈模式可以將請求傳遞給一系列過濾器,每個過濾器都可以處理請求的特定方面。

責任鏈模式的角色:

  • 抽象處理角色(Handler):定義一個處理請求的介面,包含抽象處理方法和一個後繼連線。

  • 具體處理者角色(ConcreteHandler):實現抽象處理者的處理方法,該處理方法中會進行判斷能夠處理本次請求,如果可以則將請求轉給其後繼者繼續執行處理方法。

  • 客戶類(Client)角色:建立處理鏈,並向鏈頭的具體處理者物件提交請求,它不關心處理細節和請求的傳遞過程。

typescript 程式碼實現:

// 抽象處理者
abstract class Approver {
    protected successor: Approver | null = null;

    // 設定下一個處理者
    public setSuccessor(successor: Approver): void {
        this.successor = successor;
    }

    // 處理請求的方法,由子類實現
    public abstract handleRequest(leaveDays: number): void;
}

// 具體處理者 - 組織領導
class TeamLeader extends Approver {
    public handleRequest(leaveDays: number): void {
        if (leaveDays <= 2) {
            console.log(`TeamLeader approved ${leaveDays} day(s) leave.`);
        } else if (this.successor) {
            this.successor.handleRequest(leaveDays);
        }
    }
}

// 具體處理者 - 部門領導
class DepartmentManager extends Approver {
    public handleRequest(leaveDays: number): void {
        if (leaveDays <= 5) {
            console.log(`DepartmentManager approved ${leaveDays} day(s) leave.`);
        } else if (this.successor) {
            this.successor.handleRequest(leaveDays);
        }
    }
}

// 具體處理者 - 總經理
class GeneralManager extends Approver {
    public handleRequest(leaveDays: number): void {
        if (leaveDays <= 10) {
            console.log(`GeneralManager approved ${leaveDays} day(s) leave.`);
        } else {
            console.log(`Leave request for ${leaveDays} day(s) requires an executive meeting.`);
        }
    }
}

// 客戶端程式碼
const teamLeader = new TeamLeader();
const departmentManager = new DepartmentManager();
const generalManager = new GeneralManager();

// 設定責任鏈
teamLeader.setSuccessor(departmentManager);
departmentManager.setSuccessor(generalManager);

// 發起請假請求
teamLeader.handleRequest(2); // TeamLeader can handle this request
teamLeader.handleRequest(4); // DepartmentManager has to handle this request
teamLeader.handleRequest(8); // GeneralManager has to handle this request
teamLeader.handleRequest(12); // Needs executive meeting

之前我覺得洋蔥模型就是挺適合責任鏈模式的,,其是不是的!。

適合責任鏈模式的架構模式是:

  • 管道模式:管道模式將請求按順序傳遞給一系列過濾器,每個過濾器都可以修改請求或返回響應。

  • 攔截器模式:攔截器模式允許在請求和響應被處理之前和之後插入自定義邏輯。

  • 中介軟體模式:每個中介軟體元件都有機會處理請求並決定是否將請求傳遞給鏈中的下一個中介軟體。這在Redux中的中介軟體或Express.js中的請求處理中很常見。

  • 外掛模式:。每個外掛都可以獨立地處理任務,並決定是否繼續執行後續外掛。

具體參看:

  • 如何評價責任鏈模式應用的場景? - 東小西的回答 - 知乎 https://www.zhihu.com/question/485478970/answer/2685506927

中介者模式 (Mediator Pattern)

定義一個物件,它封裝了多個物件之間的互動。中介者模式使物件之間鬆散耦合,並允許它們獨立於彼此進行通訊。

中介者模式

又叫調停模式,定義一箇中介角色來封裝一系列物件之間的互動,使原有物件之間的耦合鬆散,且可以獨立地改變它們之間的互動。

中介者模式

核心思想

如果一個系統中物件之間的聯絡呈現出網狀結構,物件之間存在大量多對多關係,將導致關係及其複雜,這些物件稱為 "同事物件"。我們可以引入一箇中介者物件,使各個同事物件只跟中介者物件打交道,將同事物件之間的關係行為進行分離和封裝,使之成為一個松耦合的系統。

本質

解耦各個同事物件之間的互動關係。每個物件都持有中介者物件的引用,只跟中介者物件打交道。透過中介者物件統一管理這些互動關係,並且還可以在同事物件的邏輯上封裝自己的邏輯。

典型場景:

當需要控制多個物件之間的複雜互動時。例如

  • 在聊天室中,中介者模式可以管理使用者之間的訊息傳遞。

  • Redux 、Vuex、 MobX 都可以說是中介模式:使用中介者模式來管理應用程式狀態(store 作為中介者,負責協調應用程式中的所有狀態更新)。

結構

中介者模式包含以下主要角色:

  • 抽象中介者(Mediator)角色:它是中介者的介面,提供了同事物件註冊與轉發同事物件資訊的抽象方法。

  • 具體中介者(ConcreteMediator)角色:實現中介者介面,定義一個 List 來管理同事物件,協調各個同事角色之間的互動關係,因此它依賴於同事角色。

  • 抽象同事類(Colleague)角色:定義同事類的介面,儲存中介者物件,提供同事物件互動的抽象方法,實現所有相互影響的同事類的公共功能。

  • 具體同事類(Concrete Colleague)角色:是抽象同事類的實現者,當需要與其他同事物件互動時,由中介者物件負責後續的互動。

中介軟體模式UML圖

// 中介者介面
interface Mediator {
    notify(sender: object, event: string): void;
}

// 具體中介者
class ConcreteMediator implements Mediator {
    private component1: Component1;
    private component2: Component2;

    constructor(c1: Component1, c2: Component2) {
        this.component1 = c1;
        this.component1.setMediator(this);
        this.component2 = c2;
        this.component2.setMediator(this);
    }

    public notify(sender: object, event: string): void {
        if (event === 'A') {
            console.log('Mediator reacts on A and triggers following operations:');
            this.component2.doC();
        } else if (event === 'D') {
            console.log('Mediator reacts on D and triggers following operations:');
            this.component1.doB();
            this.component2.doC();
        }
    }
}

// 基礎元件
class BaseComponent {
    protected mediator: Mediator;

    constructor(mediator?: Mediator) {
        this.mediator = mediator!;
    }

    public setMediator(mediator: Mediator): void {
        this.mediator = mediator;
    }
}

// 具體元件1
class Component1 extends BaseComponent {
    public doA(): void {
        console.log('Component 1 does A.');
        this.mediator.notify(this, 'A');
    }

    public doB(): void {
        console.log('Component 1 does B.');
        this.mediator.notify(this, 'B');
    }
}

// 具體元件2
class Component2 extends BaseComponent {
    public doC(): void {
        console.log('Component 2 does C.');
        this.mediator.notify(this, 'C');
    }

    public doD(): void {
        console.log('Component 2 does D.');
        this.mediator.notify(this, 'D');
    }
}

// 客戶端程式碼
const c1 = new Component1();
const c2 = new Component2();
const mediator = new ConcreteMediator(c1, c2);

console.log('Client triggers operation A.');
c1.doA();

console.log('Client triggers operation D.');
c2.doD();

在這個示例中,ConcreteMediator 類透過實現 Mediator 介面充當中介者。Component1 和 Component2 是兩個基於 BaseComponent 的具體元件,它們透過中介者進行通訊,而不是直接相互呼叫。當一個元件改變狀態或需要與其他元件通訊時,它會透過中介者來協調這些互動。

適合中介者模式的架構:

  • 複雜的前端應用,特別是那些需要多個元件相互通訊的單頁應用(SPA)。

  • 當元件或類之間的通訊非常複雜且相互依賴時,使用中介者模式可以將這些通訊集中到一箇中介者物件中,從而簡化它們之間的互動。

  • 在微服務架構中,中介者可以作為服務匯流排或訊息代理,協調不同服務之間的通訊。

中介者模式適用於需要降低多個元件或服務之間複雜交雲的情況,它有助於將系統的通訊複雜性集中管理,從而使系統更加模組化和易於維護。

中介者模式VS觀察者模式

中介者模式釋出訂閱模式,微商類比

  • 通訊結構

    • 中介者模式通常用於集中控制通訊,所有元件都透過中介者來交雲,

    • 觀察者模式通常用於分散控制,主題物件直接通知其所有的觀察者。

  • 耦合度

    • 中介者模式將元件之間的通訊耦合轉移到了中介者物件上,

    • 觀察者模式則是在主題和觀察者之間建立了一種鬆散耦合。

  • 適用性

    • 中介者模式適合用於減少多個元件或類之間的直接交雲

    • 觀察者模式適合用於狀態變化需要通知多個依賴者的場景。

具體參看文章:

設計模式(二十三)----行為型模式之中介者模式 https://www.cnblogs.com/xiaoyh/p/16563324.html

訪問者模式 (Visitor Pattern)

將操作與物件結構分離,以便可以在不改變結構的情況下向物件新增新操作。

訪問者模式能將演算法與其所作用的物件隔離開來。

訪問者模式-Visitor Pattern - 操作複雜物件結構——訪問者模式(一) - 《設計模式 Java版本》 - 書棧網 · ...

訪問者模式的適用場景:

  • 當需要在一個複雜物件結構中執行與其元素無關的操作時,可以使用訪問者模式。這允許在不修改元素類的情況下新增新的操作。

  • 當需要對不同型別的物件執行不同的操作,且不希望在物件本身實現這些操作時,可以使用訪問者模式。這可以避免在物件中新增大量條件語句。

  • 當需要分離一組類的操作邏輯時,可以使用訪問者模式。透過將操作實現移動到訪問者類中,可以使原始類更加簡單,更易於維護。

典型場景:

當需要在不修改物件結構的情況下向物件新增新操作時。例如,

  • 文件格式轉換器,可以在不改變文件元素的情況下新增新的操作,以支援不同的格式輸出。

  • 編譯器設計: 在編譯器中,訪問者模式可以用於遍歷抽象語法樹(AST)並執行不同的操作,比如語法檢查、語義分析、程式碼生成等。

  • 文件解析和處理: 當需要對複雜的文件結構進行解析和處理時,訪問者模式可以用於遍歷文件元素並執行不同的操作,比如格式轉換、資訊提取等。

  • 圖形化介面庫: 在圖形化介面庫中,訪問者模式可以用於處理圖形元素的遍歷和操作,比如在 GUI 中實現佈局、事件處理等功能。

  • 網路協議解析: 在網路程式設計中,訪問者模式可以用於解析和處理複雜的協議資料包,執行不同的操作,如資料包解析、錯誤檢測等。

訪問者模式優缺點

訪問者模式的優點:

  • 分離操作和資料結構:訪問者模式允許將操作邏輯與資料結構分離,使得操作邏輯易於維護和擴充套件。

  • 單一職責原則:每個訪問者物件負責一組特定的操作,使得操作實現更加清晰和簡潔。

  • 擴充套件性:透過新增新的訪問者類,可以很容易地向現有物件結構新增新功能,而無需修改原始類。

訪問者模式的缺點:

  • 物件結構不穩定:如果物件結構頻繁變化,訪問者模式可能不適合。因為每次新增新的元素類,都需要修改訪問者介面及其所有實現。

  • 訪問者類可能變得複雜:如果訪問者需要處理許多不同型別的元素,那麼訪問者類可能會變得很複雜。這會導致難以維護和理解的程式碼。

  • 破壞封裝:訪問者模式需要暴露物件結構的內部細節給訪問者物件,這可能會導致物件結構的封裝性受到破壞。

  • 不適用於所有場景:訪問者模式主要適用於物件結構穩定且需要對其執行多種操作的場景。對於需要頻繁修改物件結構的專案,訪問者模式可能並不適用。

結構組成

  1. 抽象訪問者(Visitor): 定義了對每個具體元素類所提供的訪問操作介面。

  2. 具體訪問者(ConcreteVisitor): 實現了抽象訪問者定義的介面,提供了具體的訪問操作。

  3. 抽象元素(Element): 宣告瞭接受訪問者操作的介面。

  4. 具體元素(ConcreteElement): 實現了抽象元素宣告的介面,提供了接受訪問者操作的具體實現。

  5. 物件結構(Object Structure): 包含了具體元素的容器,可以是一個集合或一個複雜的結構,用於存放具體元素。

  6. 客戶端(Client): 透過訪問者模式訪問物件結構中的元素。

4.webp

typescript程式碼的簡單實現

/// 訪問者介面
interface Visitor {
    visitConcreteElementA(element: ConcreteElementA): void;
    visitConcreteElementB(element: ConcreteElementB): void;
}

// 具體訪問者
class ConcreteVisitor1 implements Visitor {
    public visitConcreteElementA(element: ConcreteElementA): void {
        console.log(`ConcreteVisitor1: Visiting ${element.operationA()}`);
    }

    public visitConcreteElementB(element: ConcreteElementB): void {
        console.log(`ConcreteVisitor1: Visiting ${element.operationB()}`);
    }
}

// 另一個具體訪問者
class ConcreteVisitor2 implements Visitor {
    public visitConcreteElementA(element: ConcreteElementA): void {
        console.log(`ConcreteVisitor2: Visiting ${element.operationA()}`);
    }

    public visitConcreteElementB(element: ConcreteElementB): void {
        console.log(`ConcreteVisitor2: Visiting ${element.operationB()}`);
    }
}

// 元素介面
interface Element {
    accept(visitor: Visitor): void;
}

// 具體元素A
class ConcreteElementA implements Element {
    public accept(visitor: Visitor): void {
        visitor.visitConcreteElementA(this);
    }

    public operationA(): string {
        return 'ConcreteElementA';
    }
}

// 具體元素B
class ConcreteElementB implements Element {
    public accept(visitor: Visitor): void {
        visitor.visitConcreteElementB(this);
    }

    public operationB(): string {
        return 'ConcreteElementB';
    }
}

// 客戶端程式碼
const elements: Element[] = [
    new ConcreteElementA(),
    new ConcreteElementB(),
];

const visitor1: Visitor = new ConcreteVisitor1();
const visitor2: Visitor = new ConcreteVisitor2();

elements.forEach((element) => {
    element.accept(visitor1);
    element.accept(visitor2);
});

具體的例子:

// 訪問者介面
interface ComputerPartVisitor {
    visitKeyboard(keyboard: Keyboard): void;
    visitMouse(mouse: Mouse): void;
    visitMonitor(monitor: Monitor): void;
}

// 計算機外設介面
interface ComputerPart {
    accept(visitor: ComputerPartVisitor): void;
}

// 鍵盤類
class Keyboard implements ComputerPart {
    public accept(visitor: ComputerPartVisitor): void {
        visitor.visitKeyboard(this);
    }

    public type(): void {
        console.log('Keyboard typing...');
    }
}

// 滑鼠類
class Mouse implements ComputerPart {
    public accept(visitor: ComputerPartVisitor): void {
        visitor.visitMouse(this);
    }

    public click(): void {
        console.log('Mouse clicking...');
    }
}

// 顯示器類
class Monitor implements ComputerPart {
    public accept(visitor: ComputerPartVisitor): void {
        visitor.visitMonitor(this);
    }

    public display(): void {
        console.log('Monitor displaying...');
    }
}

// 通用計算機外設訪問者
class GeneralComputerPartVisitor implements ComputerPartVisitor {
    public visitKeyboard(keyboard: Keyboard): void {
        keyboard.type();
    }

    public visitMouse(mouse: Mouse): void {
        mouse.click();
    }

    public visitMonitor(monitor: Monitor): void {
        monitor.display();
    }
}

// 顯示器訪問者
class MonitorVisitor implements ComputerPartVisitor {
    public visitKeyboard(keyboard: Keyboard): void {
        // 不關心鍵盤
    }

    public visitMouse(mouse: Mouse): void {
        // 不關心滑鼠
    }

    public visitMonitor(monitor: Monitor): void {
        console.log('Applying special monitor calibration...');
        monitor.display();
    }
}

// 客戶端程式碼
const parts: ComputerPart[] = [
    new Keyboard(),
    new Mouse(),
    new Monitor(),
];

const generalVisitor: ComputerPartVisitor = new GeneralComputerPartVisitor();
const monitorVisitor: ComputerPartVisitor = new MonitorVisitor();

parts.forEach((part) => {
    part.accept(generalVisitor); // 應用通用訪問者
    part.accept(monitorVisitor); // 應用顯示器訪問者
});

在這個示例中,ComputerPartVisitor 介面定義了訪問者需要實現的方法,GeneralComputerPartVisitor 是一個通用的訪問者,它對所有外設執行操作。MonitorVisitor 是一個專門為顯示器設計的訪問者,它只對顯示器執行特殊的操作。

ComputerPart 介面定義了一個 accept 方法,Keyboard、Mouse 和 Monitor 是實現了該介面的具體外設類。每個外設類都實現了 accept 方法,該方法接受一個訪問者並呼叫其訪問方法。

具體參看:

  • 【設計模式】訪問者模式 https://zhuanlan.zhihu.com/p/401974000

  • [設計模式]24. 訪問者模式 https://zhuanlan.zhihu.com/p/448513112

  • 操作複雜物件結構——訪問者模式(一) https://www.bookstack.cn/read/design-pattern-java/操作複雜物件結構——訪問者模式(一).md

直譯器模式 (Interpreter Pattern)

定義一個語言的文法,並提供一個直譯器來解釋該語言中的句子。

自定義語言的實現——直譯器模式(一) - 圖1自定義語言的實現——直譯器模式(二) - 圖1

典型場景:

當需要解釋一個特定領域的語言時。例如,

  • 程式語言直譯器或表示式計算器,解釋特定的語法或表示式。

  • 在編譯器中,直譯器模式可以解釋原始碼中的語句。

在直譯器模式結構圖中包含如下幾個角色:

  • AbstractExpression(抽象表示式):在抽象表示式中宣告瞭抽象的解釋操作,它是所有終結符表示式和非終結符表示式的公共父類。

  • TerminalExpression(終結符表示式):終結符表示式是抽象表示式的子類,它實現了與文法中的終結符相關聯的解釋操作,在句子中的每一個終結符都是該類的一個例項。通常在一個直譯器模式中只有少數幾個終結符表示式類,它們的例項可以透過非終結符表示式組成較為複雜的句子。

  • NonterminalExpression(非終結符表示式):非終結符表示式也是抽象表示式的子類,它實現了文法中非終結符的解釋操作,由於在非終結符表示式中可以包含終結符表示式,也可以繼續包含非終結符表示式,因此其解釋操作一般透過遞迴的方式來完成。

  • Context(環境類):環境類又稱為上下文類,它用於儲存直譯器之外的一些全域性資訊,通常它臨時儲存了需要解釋的語句。

自定義語言的實現——直譯器模式(三) - 圖1

typescript實現

// 表示式介面
interface Expression {
    interpret(): number;
}

// 數字表示式
class NumberExpression implements Expression {
    private value: number;

    constructor(value: number) {
        this.value = value;
    }

    public interpret(): number {
        return this.value;
    }
}

// 加法表示式
class AddExpression implements Expression {
    private leftExpression: Expression;
    private rightExpression: Expression;

    constructor(left: Expression, right: Expression) {
        this.leftExpression = left;
        this.rightExpression = right;
    }

    public interpret(): number {
        return this.leftExpression.interpret() + this.rightExpression.interpret();
    }
}

// 減法表示式
class SubtractExpression implements Expression {
    private leftExpression: Expression;
    private rightExpression: Expression;

    constructor(left: Expression, right: Expression) {
        this.leftExpression = left;
        this.rightExpression = right;
    }

    public interpret(): number {
        return this.leftExpression.interpret() - this.rightExpression.interpret();
    }
}

// 客戶端程式碼
const expression: Expression = new AddExpression(
    new NumberExpression(5),
    new SubtractExpression(
        new NumberExpression(10),
        new NumberExpression(3)
    )
);

console.log(expression.interpret()); // 輸出結果應該是 5 + (10 - 3) = 12

直譯器模式可能與以下幾種設計模式混淆:

  1. 命令模式(Command Pattern):

    • 相似之處:命令模式和直譯器模式都涉及到對操作的封裝。

    • 區別

      • 命令模式封裝了一系列操作和引數,使得可以將請求作為一個物件儲存、傳遞和呼叫。

      • 直譯器模式則是用於定義一個語言的文法,並提供一個直譯器來解釋語言中的句子。

    • 如何區分

      • 如果目的是建立一個可以解釋和執行預定義語法和規則的系統,那麼使用直譯器模式。

      • 如果目的是將操作封裝為物件,以便支援撤銷操作、日誌記錄或事務等,則使用命令模式。

  2. 策略模式(Strategy Pattern):

    • 相似之處:策略模式和直譯器模式都允許在執行時更改物件的行為。

    • 區別

      • 策略模式透過定義一系列可互換的演算法來允許物件改變其行為,

      • 直譯器模式專注於定義和解釋語言的文法。

    • 如何區分

      • 如果你需要在不同演算法之間切換以完成相同的任務,那麼使用策略模式。

      • 如果你需要實現一個特定的語言直譯器,那麼使用直譯器模式。

  3. 訪問者模式(Visitor Pattern):

    • 相似之處:訪問者模式和直譯器模式都涉及到對物件結構的操作。

    • 區別

      • 訪問者模式主要用於在不改變元素類的情況下增加操作,它透過將操作邏輯外部化到訪問者中來實現。

      • 直譯器模式則是用於解釋給定語言的句子。

    • 如何區分:如果你需要對一個複雜的物件結構執行多種不同且不相關的操作,並且希望能夠在不修改物件結構的情況下新增新操作,那麼使用訪問者模式。如果你需要實現一個直譯器來解釋語言的句子,那麼使用直譯器模式。

  4. 組合模式(Composite Pattern):

    • 相似之處:組合模式和直譯器模式都可以用來表示物件的部分-整體層次結構。

    • 區別

      • 組合模式主要用於表示和管理物件的層次結構,使得客戶端可以統一對待單個物件和組合物件。

      • 直譯器模式則用於定義語言的文法和解釋句子。

    • 如何區分

      • 如果你的目標是簡化對單個物件和組合物件的操作,那麼使用組合模式。

      • 如果你的目標是實現一個直譯器來解釋語言的句子,那麼使用直譯器模式。

狀態模式 (State Pattern)

允許物件在內部狀態改變時改變其行為。

狀態模式狀態機到狀態模式.webp

狀態模式與有限狀態機的概念緊密相關。

image.png狀態機模式演示

典型場景:

當物件的內部狀態會影響其行為時。例如,

  • 在狀態機中,狀態模式可以定義物件在不同狀態下的行為。

  • 文字編輯器中的文字選擇工具,根據選擇的工具(如選擇、繪製、擦除)改變行為。

結構

狀態模式包含以下主要角色。

  • 環境(Context)角色:也稱為上下文,它定義了客戶程式需要的介面,維護一個當前狀態,並將與狀態相關的操作委託給當前狀態物件來處理。

  • 抽象狀態(State)角色:定義一個介面,用以封裝環境物件中的特定狀態所對應的行為。

  • 具體狀態(Concrete State)角色:實現抽象狀態所對應的行為。

typescript 程式碼實現:

// 狀態介面
interface State {
    handle(context: Context): void;
}

// 具體狀態A
class ConcreteStateA implements State {
    public handle(context: Context): void {
        console.log('Handling state A.');
        context.setState(new ConcreteStateB());
    }
}

// 具體狀態B
class ConcreteStateB implements State {
    public handle(context: Context): void {
        console.log('Handling state B.');
        context.setState(new ConcreteStateA());
    }
}

// 上下文類
class Context {
    private state: State;

    constructor(state: State) {
        this.state = state;
    }

    public setState(state: State): void {
        this.state = state;
    }

    public request(): void {
        this.state.handle(this);
    }
}

// 客戶端程式碼
const context = new Context(new ConcreteStateA());
context.request(); // Handling state A.
context.request(); // Handling state B.
context.request(); // Handling state A.

前端開原始碼案例:

在前端開發中,狀態模式可以用於管理元件或應用的狀態。例如,Redux庫就是一個狀態管理庫,它在某種程度上體現了狀態模式的思想。在Redux中,狀態轉換邏輯透過純函式(reducers)來管理,每個reducer根據當前狀態和傳入的action來計算新狀態。

// Redux中的reducer示例
function visibilityFilter(state = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter;
    default:
      return state;
  }
}

在這個例子中,visibilityFilter 函式是一個reducer,它根據當前狀態和action來返回新狀態,這與狀態模式中的狀態轉換邏輯類似。

推薦本站的:《狀態機圖教程 (UML State Diagram) 》、《從有限狀態機、圖靈機到現代計算機——排版整理筆記版》、《React 原始碼剖析系列—生命週期的管理藝術—有限狀態機

狀態模式和策略模式的區別

  • 狀態模式強調的是一個物件內部狀態的改變會導致其行為的改變,狀態是物件的內部屬性,通常由物件自身管理和控制。

  • 策略模式強調的是一系列演算法的可互換性,策略是從物件外部傳入的,通常由客戶端程式碼控制和管理。

狀態模式和策略模式都可以避免多重條件選擇語句,它們的類圖也十分相似,它們都有一個上下文(Context)、一些策略類(Strategy)或者狀態類(State),上下文把請求委託給這些類來執行。

如果只是單純的多情況if else ,各個條件之間是平等又平行的,沒有任何聯絡,就可以選擇使用策略模式,程式碼可能會更優雅易維護,成本會低一些,策略模式要求瞭解策略類中的演算法邏輯,以便它們之間的互相替換;狀態模式中,狀態和狀態對應的行為以及狀態間的切換是早就規定好的,都是在內部發生的,不需要了解具體的細節,類似IM socket的場景,狀態機會更加優雅。

具體參看:

  • 沒有徹底消除if-else還能算是狀態模式嗎? - 清泓y的回答 - 知乎 https://www.zhihu.com/question/537269355/answer/3203869280

  • 淺談狀態模式和狀態機 https://zhuanlan.zhihu.com/p/408738144

如果只是單純的多情況if else ,各個條件之間是平等又平行的,沒有任何聯絡,就可以選擇使用策略模式,程式碼可能會更優雅易維護,成本會低一些,策略模式要求瞭解策略類中的演算法邏輯,以便它們之間的互相替換;狀態模式中,狀態和狀態對應的行為以及狀態間的切換是早就規定好的,都是在內部發生的,不需要了解具體的細節,類似IM socket的場景,狀態機會更加優雅。

備忘錄模式 (Memento Pattern)

捕獲一個物件的內部狀態,以便以後可以恢復該狀態。

撤銷功能的實現——備忘錄模式(一) - 圖2撤銷功能的實現——備忘錄模式(一) - 圖1

典型場景:

當需要儲存和恢復物件的內部狀態時。例如,

  • 遊戲存檔,儲存當前遊戲狀態,以便可以重新載入。

  • 在撤消操作中,備忘錄模式可以儲存物件的先前狀態,以便可以撤消對該物件的更改。

備忘錄模式結構圖中包含如下幾個角色:

  • Originator(原發器):它是一個普通類,可以建立一個備忘錄,並儲存它的當前內部狀態,也可以使用備忘錄來恢復其內部狀態,一般將需要儲存內部狀態的類設計為原發器。

  • Memento(備忘錄):儲存原發器的內部狀態,根據原發器來決定儲存哪些內部狀態。備忘錄的設計一般可以參考原發器的設計,根據實際需要確定備忘錄類中的屬性。需要注意的是,除了原發器本身與負責人類之外,備忘錄物件不能直接供其他類使用,原發器的設計在不同的程式語言中實現機制會有所不同。

  • Caretaker(負責人):負責人又稱為管理者,它負責儲存備忘錄,但是不能對備忘錄的內容進行操作或檢查。在負責人類中可以儲存一個或多個備忘錄物件,它只負責儲存物件,而不能修改物件,也無須知道物件的實現細節。

理解備忘錄模式並不難,但關鍵在於如何設計備忘錄類和負責人類。由於在備忘錄中儲存的是原發器的中間狀態,因此需要防止原發器以外的其他物件訪問備忘錄,特別是不允許其他物件來修改備忘錄。

撤銷功能的實現——備忘錄模式(三) - 圖1

typescript程式碼實現

// 備忘錄類
class ChessMemento {
    private state: string;

    constructor(state: string) {
        this.state = state;
    }

    public getState(): string {
        return this.state;
    }
}

// 發起人類
class ChessGame {
    private state: string;

    constructor() {
        this.state = ''; // 初始狀態,可以是棋盤的表示等
    }

    public playMove(move: string): void {
        // 執行棋步並更新狀態
        this.state += move + ';';
    }

    public save(): ChessMemento {
        // 儲存當前狀態
        return new ChessMemento(this.state);
    }

    public restore(memento: ChessMemento): void {
        // 恢復到之前的狀態
        this.state = memento.getState();
    }

    public showState(): void {
        console.log(this.state);
    }
}

// 管理者類
class ChessCaretaker {
    private mementos: ChessMemento[] = [];

    public saveMemento(memento: ChessMemento): void {
        this.mementos.push(memento);
    }

    public getMemento(index: number): ChessMemento {
        return this.mementos[index];
    }
}

// 客戶端程式碼
const game = new ChessGame();
const caretaker = new ChessCaretaker();

// 玩家下棋步
game.playMove('e4');
game.playMove('e5');
caretaker.saveMemento(game.save()); // 儲存當前狀態

game.playMove('Nf3');
caretaker.saveMemento(game.save()); // 儲存新狀態

// 悔棋操作
game.restore(caretaker.getMemento(0)); // 恢復到第一次儲存的狀態
game.showState(); // 顯示當前棋盤狀態

如果用直譯器來寫呢?

// 定義一個抽象表示式介面
interface Expression {
  interpret(): void;
}

// 定義一個具體表示式(移動棋子)
class MoveExpression implements Expression {
  private chessboard: Chessboard;
  private from: string;
  private to: string;

  constructor(chessboard: Chessboard, from: string, to: string) {
    this.chessboard = chessboard;
    this.from = from;
    this.to = to;
  }

  interpret(): void {
    this.chessboard.move(this.from, this.to);
  }
}

// 定義一個具體表示式(悔棋)
class UndoExpression implements Expression {
  private chessboard: Chessboard;

  constructor(chessboard: Chessboard) {
    this.chessboard = chessboard;
  }

  interpret(): void {
    this.chessboard.undo();
  }
}

// 定義一個直譯器(棋盤)
class Chessboard {
  private board: string[][];

  constructor() {
    this.board = [['♜', '♞', '♝', '♛', '♚', '♝', '♞', '♜'],
                   ['♟', '♟', '♟', '♟', '♟', '♟', '♟', '♟'],
                   [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                   [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                   [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                   [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                   ['♙', '♙', '♙', '♙', '♙', '♙', '♙', '♙'],
                   ['♖', '♘', '♗', '♕', '♔', '♗', '♘', '♖']];
  }

  move(from: string, to: string): void {
    // 移動棋子
  }

  undo(): void {
    // 悔棋
  }

  interpret(expression: Expression): void {
    expression.interpret();
  }
}

// 使用直譯器模式
const chessboard = new Chessboard();
const moveExpression = new MoveExpression(chessboard, 'e2', 'e4');
const undoExpression = new UndoExpression(chessboard);

chessboard.interpret(moveExpression); // 移動棋子
chessboard.interpret(undoExpression); // 悔棋

狀態模式(State Pattern)和備忘錄模式(Memento Pattern)都是行為設計模式,它們在處理物件狀態方面有所涉及,但它們的目的、實現方式和應用場景有顯著的不同。

狀態模式

  • 目的:狀態模式允許一個物件在其內部狀態改變時改變它的行為。物件會根據自身的狀態變化來選擇不同的行為方式。

  • 應用場景:當一個物件的行為取決於它的狀態,並且它必須在執行時根據狀態改變它的行為時,狀態模式是合適的。狀態模式通常用於實現有狀態的物件,其中物件的行為會隨著狀態的改變而改變。

  • 實現方式:狀態模式透過定義一個狀態介面和一系列實現該介面的具體狀態類來實現。上下文物件透過持有狀態介面的引用來維護當前狀態,並將狀態相關的行為委託給當前狀態物件處理。

備忘錄模式

  • 目的:備忘錄模式用於捕獲和恢復物件的內部狀態,而不暴露其實現細節。這種模式特別適用於實現撤銷(undo)和恢復(redo)功能。

  • 應用場景:當需要儲存物件的歷史狀態,並且可能需要在將來某個時刻恢復這些狀態時,備忘錄模式是合適的。備忘錄模式通常用於實現撤銷和恢復操作,如編輯器的撤銷功能或遊戲的存檔和載入。

  • 實現方式:備忘錄模式透過建立一個備忘錄物件來儲存當前狀態,該備忘錄物件對其他物件不可見,從而保證了封裝性。發起人物件負責建立和使用備忘錄來儲存和恢復自身狀態,而管理者物件負責儲存備忘錄的歷史記錄。

區別總結

  • 設計意圖不同

    • 狀態模式關注於物件行為的變化,它透過狀態物件來改變物件的行為。

    • 備忘錄模式關注於物件狀態的儲存和恢復,它透過備忘錄物件來儲存物件的歷史狀態。

  • 應用場景不同

    • 狀態模式適用於物件的狀態會影響其行為的場景,

    • 備忘錄模式適用於需要儲存和恢復物件狀態的場景。

  • 實現方式不同

    • 狀態模式將每個狀態的行為封裝在獨立的狀態物件中,上下文物件透過這些狀態物件來改變其行為。

    • 備忘錄模式透過備忘錄物件來儲存狀態,而不改變物件的結構或暴露其實現細節。

儘管狀態模式和備忘錄模式都涉及物件的狀態,但它們的設計目的和使用方式有明顯的區別。狀態模式關注於物件行為的動態變化,而備忘錄模式關注於物件狀態的儲存和恢復。

圍棋,狀態模式

// 狀態介面
interface ChessState {
    playMove(move: string): void;
    showState(): void;
}

// 具體狀態類
class ChessMoveState implements ChessState {
    private moves: string[] = [];

    constructor(moves?: string[]) {
        if (moves) {
            this.moves = moves.slice(); // 建立移動列表的副本
        }
    }

    public playMove(move: string): void {
        this.moves.push(move);
    }

    public showState(): void {
        console.log(this.moves.join('; '));
    }

    public clone(): ChessMoveState {
        // 建立當前狀態的副本
        return new ChessMoveState(this.moves);
    }
}

// 上下文類
class ChessGame {
    private currentState: ChessMoveState;

    constructor() {
        this.currentState = new ChessMoveState();
    }

    public playMove(move: string): void {
        this.currentState.playMove(move);
    }

    public save(): ChessMoveState {
        return this.currentState.clone();
    }

    public restore(state: ChessMoveState): void {
        this.currentState = state.clone();
    }

    public showState(): void {
        this.currentState.showState();
    }
}

// 客戶端程式碼
const game = new ChessGame();
const savedStates: ChessMoveState[] = [];

// 玩家下棋步
game.playMove('e4');
game.playMove('e5');
savedStates.push(game.save()); // 儲存當前狀態

game.playMove('Nf3');
savedStates.push(game.save()); // 儲存新狀態

// 悔棋操作
game.restore(savedStates[0]); // 恢復到第一次儲存的狀態
game.showState(); // 顯示當前棋盤狀態

命令模式 (Command Pattern)

將請求封裝成物件,從而使您可以用不同的方式對請求進行引數化、排隊和執行。

請求傳送者與接收者解耦——命令模式(一) - 圖1

典型場景:

當需要將請求與執行請求的物件解耦時。例如

  • 遙控器按鈕,每個按鈕執行不同的命令,如開/關電視、調節音量等。

  • 在文字編輯器中,命令模式可以將編輯操作(如剪下、複製和貼上)封裝成物件,以便可以輕鬆地撤消和重做這些操作。

命令模式結構圖中包含如下幾個角色:

  • Command(抽象命令類):抽象命令類一般是一個抽象類或介面,在其中宣告瞭用於執行請求的execute()等方法,透過這些方法可以呼叫請求接收者的相關操作。

  • ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了在抽象命令類中宣告的方法,它對應具體的接收者物件,將接收者物件的動作繫結其中。在實現execute()方法時,將呼叫接收者物件的相關操作(Action)。

  • Invoker(呼叫者):呼叫者即請求傳送者,它透過命令物件來執行請求。一個呼叫者並不需要在設計時確定其接收者,因此它只與抽象命令類之間存在關聯關係。在程式執行時可以將一個具體命令物件注入其中,再呼叫具體命令物件的execute()方法,從而實現間接呼叫請求接收者的相關操作。

  • Receiver(接收者):接收者執行與請求相關的操作,它具體實現對請求的業務處理。

請求傳送者與接收者解耦——命令模式(一) - 圖3

JavaScript程式碼實現

// 命令介面
interface Command {
    execute(): void;
    undo(): void;
}

// 電燈類
class Light {
    public on(): void {
        console.log('Light is on');
    }

    public off(): void {
        console.log('Light is off');
    }
}

// 風扇類
class Fan {
    public start(): void {
        console.log('Fan is running');
    }

    public stop(): void {
        console.log('Fan is stopped');
    }
}

// 電燈開關命令
class LightOnCommand implements Command {
    private light: Light;

    constructor(light: Light) {
        this.light = light;
    }

    public execute(): void {
        this.light.on();
    }

    public undo(): void {
        this.light.off();
    }
}

// 電燈關命令
class LightOffCommand implements Command {
    private light: Light;

    constructor(light: Light) {
        this.light = light;
    }

    public execute(): void {
        this.light.off();
    }

    public undo(): void {
        this.light.on();
    }
}

// 風扇開命令
class FanStartCommand implements Command {
    private fan: Fan;

    constructor(fan: Fan) {
        this.fan = fan;
    }

    public execute(): void {
        this.fan.start();
    }

    public undo(): void {
        this.fan.stop();
    }
}

// 風扇關命令
class FanStopCommand implements Command {
    private fan: Fan;

    constructor(fan: Fan) {
        this.fan = fan;
    }

    public execute(): void {
        this.fan.stop();
    }

    public undo(): void {
        this.fan.start();
    }
}

// 呼叫者類
class RemoteControl {
    private onCommands: Command[];
    private offCommands: Command[];
    private undoCommand: Command;

    constructor() {
        this.onCommands = [];
        this.offCommands = [];
        this.undoCommand = null!;
    }

    public setCommand(slot: number, onCommand: Command, offCommand: Command): void {
        this.onCommands[slot] = onCommand;
        this.offCommands[slot] = offCommand;
    }


轉載本站文章《再談23種設計模式(3):行為型模式(學習筆記)》,
請註明出處:https://www.zhoulujun.cn/html/theory/engineering/model/9086.html

相關文章