使用AI学习设计模式

193 阅读29分钟

设计模式是一种被广泛应用于软件开发中的经验总结,它是对软件设计中常见问题的解决方案的描述。设计模式提供了一种通用的解决方案,可以帮助开发人员在软件开发过程中更加高效地解决问题,提高软件的可维护性、可扩展性和可重用性。

设计模式通常包括三个要素:模式名称、问题描述和解决方案。模式名称是对该模式的简单描述,问题描述是该模式所解决的问题的描述,解决方案则是该模式的具体实现方式。设计模式通常被分为三类:创建型模式、结构型模式和行为型模式。

创建型模式:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式等。这些模式主要关注如何创建对象,以及如何避免重复创建对象,提高系统的灵活性和可扩展性。

结构型模式:适配器模式、桥接模式、装饰器模式、组合模式、外观模式、享元模式、代理模式等。这些模式主要关注如何组合类和对象以形成更大的结构,以及如何简化类和对象之间的交互。

行为型模式:模板方法模式、策略模式、命令模式、职责链模式、状态模式、观察者模式、中介者模式、备忘录模式、访问者模式、解释器模式等。这些模式主要关注对象之间的交互和职责分配,以及如何在系统中实现松耦合和高内聚。

创建型

单例模式

概念

单例模式(Singleton Pattern)是一种创建型设计模式,用于限制一个类只能创建一个实例的模式。它确保全局只有一个实例,并提供全局访问点。

单例模式通常包括一个私有的构造函数和一个静态的实例变量,以及一个静态的访问方法。在访问方法中,如果实例变量不存在,则创建一个新的实例并返回,否则直接返回现有的实例。

单例模式的优点包括:

  • 提供全局访问点,方便了对象的访问和管理。
  • 可以控制对象的创建过程,例如可以延迟对象的创建时间,或者根据需要动态地创建对象。

单例模式的缺点包括:

  • 单例模式可能会导致代码的耦合度增加,因为它将对象的创建和管理都集中在一个类中。
  • 单例模式可能会导致代码的可测试性降低,因为它难以模拟和替换单例对象。

使用场景

  • 需要确保全局只有一个实例的场景,例如全局的配置对象、全局的日志对象等。
  • 需要提供全局访问点的场景,例如全局的缓存对象、全局的数据库连接对象等。
  • 需要控制对象的创建过程的场景,例如需要延迟对象的创建时间、需要根据需要动态地创建对象等。
  • 需要避免重复创建对象的场景,例如创建对象的开销较大、创建对象的数量较多等。

总之,单例模式适用于需要确保全局只有一个实例,并提供全局访问点的场景。它可以帮助我们控制对象的创建过程,避免重复创建对象,提高代码的性能和可维护性。

代码案例

class Singleton {
  private static instance: Singleton;

  private constructor() {}

  public static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }

  public doSomething(): void {
    console.log("Doing something...");
  }
}

// 使用示例
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // true
singleton1.doSomething(); // Doing something...

使用单例模式来创建Redis客户端,以确保全局只有一个Redis客户端实例,并提供全局访问点。以下是一个示例:

import redis from 'redis';

type RedisClientConstructor = new (...args: any[]) => redis.RedisClient;

function createRedisClientSingleton(Constructor: RedisClientConstructor): redis.RedisClient {
  let instance: redis.RedisClient;

  return function (this: any, ...args: any[]) {
    if (!instance) {
      instance = new Constructor(...args);
    }
    return instance;
  } as any;
}

// 创建Redis客户端单例
const RedisClientSingleton = createRedisClientSingleton(redis.createClient);

// 客户端代码
const client1 = RedisClientSingleton();
const client2 = RedisClientSingleton();

console.log(client1 === client2); // 输出 true

结构性

适配器模式

概念

适配器模式是一种结构型设计模式,它允许将不兼容的对象包装在适配器中,以便它们可以共同工作。适配器模式通常用于将现有的类或接口转换为另一个接口,以便客户端代码可以使用它们而不需要修改其代码。

适配器模式包括以下几个角色:

  • 目标接口(Target):客户端代码期望的接口,它定义了客户端代码可以使用的方法。
  • 适配器(Adapter):将不兼容的对象包装在适配器中,以便它们可以共同工作。适配器实现了目标接口,并将客户端代码的请求转换为被适配对象的请求。
  • 被适配对象(Adaptee):需要被适配的对象,它定义了客户端代码不能直接使用的方法。

适配器模式的优点包括:

  • 可以将现有的类或接口转换为另一个接口,以便客户端代码可以使用它们而不需要修改其代码。
  • 可以提高代码的复用性和可维护性,因为它可以将不同的类或接口组合在一起,以实现新的功能。
  • 可以提高代码的灵活性和可扩展性,因为它可以在不修改现有代码的情况下添加新的功能。

适配器模式的缺点包括:

  • 可能会增加代码的复杂性,因为它需要引入新的类或接口来实现适配器。
  • 可能会影响代码的性能,因为它需要进行额外的转换和处理。

总之,适配器模式是一种非常有用的设计模式,它可以将不兼容的对象包装在适配器中,以便它们可以共同工作。它可以提高代码的复用性、可维护性、灵活性和可扩展性,但也可能会增加代码的复杂性和影响代码的性能。

使用场景

适配器模式的使用场景包括:

  • 将一个类的接口转换成客户端所期望的另一个接口。例如,当一个类的接口与另一个类的接口不兼容时,可以使用适配器模式将其转换为可兼容的接口。
  • 在不修改已有代码的情况下,使得已有的类能够与其他类一起工作。例如,当需要使用一个已经存在的类,但是它的接口与系统的其他部分不兼容时,可以使用适配器模式来适配该类。
  • 将多个类的接口统一成一个接口。例如,当需要使用多个类的功能,但是它们的接口不一致时,可以使用适配器模式将它们的接口统一成一个接口。
  • 在系统中引入一个新的类,以与已有的类协同工作。例如,当需要引入一个新的类来与已有的类一起工作,但是它们的接口不兼容时,可以使用适配器模式来适配新的类。

总之,适配器模式适用于需要将不兼容的接口转换为可兼容的接口的情况,以实现不同类之间的协同工作。

代码示例

interface Rectangle {
  setWidth(width: number): void;
  setHeight(height: number): void;
  getArea(): number;
}

class LegacyRectangle {
  constructor(private width: number, private height: number) {}

  getWidth(): number {
    return this.width;
  }

  getHeight(): number {
    return this.height;
  }
}

class RectangleAdapter implements Rectangle {
  private legacyRectangle: LegacyRectangle;

  constructor(width: number, height: number) {
    this.legacyRectangle = new LegacyRectangle(width, height);
  }

  setWidth(width: number) {
    this.legacyRectangle = new LegacyRectangle(width, this.legacyRectangle.getHeight());
  }

  setHeight(height: number) {
    this.legacyRectangle = new LegacyRectangle(this.legacyRectangle.getWidth(), height);
  }

  getArea(): number {
    return this.legacyRectangle.getWidth() * this.legacyRectangle.getHeight();
  }
}

// 客户端代码
let rectangle: Rectangle = new RectangleAdapter(10, 20);
console.log(rectangle.getArea()); // 输出 200

rectangle.setWidth(5);
console.log(rectangle.getArea()); // 输出 100

rectangle.setHeight(10);
console.log(rectangle.getArea()); // 输出 50

组合模式

概念

组合模式是一种结构型设计模式,它将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

组合模式包含以下角色:

  • 抽象构件(Component):定义组合中所有对象的通用接口,可以是抽象类或接口。抽象构件中定义了一些通用的操作,例如添加子节点、删除子节点、获取子节点等。
  • 叶子构件(Leaf):表示组合中的叶子节点对象,叶子节点没有子节点。
  • 组合构件(Composite):表示组合中的非叶子节点对象,组合节点包含一个或多个子节点,子节点可以是叶子节点或组合节点。

在组合模式中,叶子节点和组合节点都实现了抽象构件中定义的通用接口,这样用户就可以对单个对象和组合对象进行统一处理。组合模式使得用户可以忽略对象与组合对象之间的差异,从而更加方便地进行操作。同时,组合模式也提高了系统的灵活性和可扩展性,因为可以很容易地添加新的叶子节点或组合节点。

应用场景

  • 树形结构:当需要处理树形结构数据时,可以使用组合模式来实现。例如文件系统、菜单系统等。
  • 部分-整体关系:当需要处理部分-整体关系时,可以使用组合模式来实现。例如汽车由引擎、车轮、座椅等部件组成,可以使用组合模式来表示。
  • 统一接口:当需要对单个对象和组合对象进行统一处理时,可以使用组合模式来实现。例如对于文件系统中的文件和文件夹,我们可以使用相同的接口来进行操作。

递归结构:当需要处理递归结构时,可以使用组合模式来实现。例如 HTML 文档中的 DOM 树结构,可以使用组合模式来表示。

代码示例

文件系统的简单示例

// 抽象节点类
abstract class Node {
  protected name: string;

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

  // 打印节点信息
  public abstract print(): void;

  // 获取节点名称
  public getName(): string {
    return this.name;
  }
}

// 叶子节点类
class File extends Node {
  constructor(name: string) {
    super(name);
  }

  // 打印文件信息
  public print(): void {
    console.log(`File: ${this.name}`);
  }
}

// 组合节点类
class Folder extends Node {
  private children: Node[] = [];

  constructor(name: string) {
    super(name);
  }

  // 添加子节点
  public add(node: Node): void {
    this.children.push(node);
  }

  // 删除子节点
  public remove(node: Node): void {
    const index = this.children.indexOf(node);
    if (index !== -1) {
      this.children.splice(index, 1);
    }
  }

  // 打印文件夹信息及其子节点信息
  public print(): void {
    console.log(`Folder: ${this.name}`);
    for (const node of this.children) {
      node.print();
    }
  }
}

// 使用示例
const root = new Folder("root");
const folder1 = new Folder("folder1");
const folder2 = new Folder("folder2");
const file1 = new File("file1");
const file2 = new File("file2");

root.add(folder1);
root.add(folder2);
folder1.add(file1);
folder2.add(file2);

root.print();

菜单系统的简单示例

// 抽象菜单项类
abstract class MenuItem {
  protected name: string;

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

  // 打印菜单项信息
  public abstract print(): void;

  // 获取菜单项名称
  public getName(): string {
    return this.name;
  }
}

// 菜单项类
class MenuLeaf extends MenuItem {
  constructor(name: string) {
    super(name);
  }

  // 打印菜单项信息
  public print(): void {
    console.log(`MenuLeaf: ${this.name}`);
  }
}

// 菜单类
class MenuComposite extends MenuItem {
  private children: MenuItem[] = [];

  constructor(name: string) {
    super(name);
  }

  // 添加子菜单或菜单项
  public add(item: MenuItem): void {
    this.children.push(item);
  }

  // 删除子菜单或菜单项
  public remove(item: MenuItem): void {
    const index = this.children.indexOf(item);
    if (index !== -1) {
      this.children.splice(index, 1);
    }
  }

  // 打印菜单信息及其子菜单信息
  public print(): void {
    console.log(`MenuComposite: ${this.name}`);
    for (const item of this.children) {
      item.print();
    }
  }
}

// 使用示例
const rootMenu = new MenuComposite("root");
const fileMenu = new MenuComposite("file");
const editMenu = new MenuComposite("edit");
const openItem = new MenuLeaf("open");
const saveItem = new MenuLeaf("save");
const copyItem = new MenuLeaf("copy");
const pasteItem = new MenuLeaf("paste");

rootMenu.add(fileMenu);
rootMenu.add(editMenu);
fileMenu.add(openItem);
fileMenu.add(saveItem);
editMenu.add(copyItem);
editMenu.add(pasteItem);

rootMenu.print();

外观模式

概念

外观模式是一种结构型设计模式,它为子系统中的一组接口提供一个统一的高层接口,从而简化了子系统的使用。

外观模式包含以下角色:

  • 外观(Facade):为子系统中的一组接口提供一个统一的高层接口,外观模式中只有一个外观类。
  • 子系统(Subsystem):实现子系统的功能,子系统中可以包含多个类。

在外观模式中,外观类封装了子系统中的一组接口,为客户端提供了一个简单的接口。客户端只需要与外观类进行交互,而不需要直接与子系统中的类进行交互。外观类将客户端的请求转发给子系统中的类进行处理,从而实现了客户端与子系统之间的解耦。

使用场景

外观模式的使用场景包括:

  • 简化复杂接口:当子系统中的接口比较复杂时,可以使用外观模式来简化接口。外观类可以封装子系统中的一组接口,为客户端提供一个简单的接口。
  • 解耦客户端和子系统:当客户端需要与多个子系统进行交互时,可以使用外观模式来解耦客户端和子系统。外观类可以封装多个子系统的接口,从而简化客户端的使用。
  • 提高安全性:当需要限制客户端对子系统的访问时,可以使用外观模式来提高系统的安全性。外观类可以限制客户端对子系统的访问,从而保护子系统的安全。
  • 统一接口:当需要为多个子系统提供一个统一的接口时,可以使用外观模式来实现。外观类可以封装多个子系统的接口,为客户端提供一个统一的接口。

总之,外观模式适用于需要简化复杂接口、解耦客户端和子系统、提高安全性、统一接口等场景。

代码示例

下面是一个简化复杂接口的实际代码示例。

假设我们要设计一个计算机系统,该系统包含 CPU、内存、硬盘等多个子系统。每个子系统都有自己的接口,客户端需要调用多个接口才能完成一个操作。为了简化客户端的使用,我们可以使用外观模式来封装这些接口,为客户端提供一个简单的接口。

下面是一个简单的示例代码:

// CPU 子系统
class CPU {
  public start(): void {
    console.log("CPU started");
  }

  public stop(): void {
    console.log("CPU stopped");
  }
}

// 内存子系统
class Memory {
  public load(): void {
    console.log("Memory loaded");
  }

  public unload(): void {
    console.log("Memory unloaded");
  }
}

// 硬盘子系统
class HardDrive {
  public read(): void {
    console.log("Hard drive read");
  }

  public write(): void {
    console.log("Hard drive write");
  }
}

// 计算机系统外观类
class Computer {
  private cpu: CPU;
  private memory: Memory;
  private hardDrive: HardDrive;

  constructor() {
    this.cpu = new CPU();
    this.memory = new Memory();
    this.hardDrive = new HardDrive();
  }

  public start(): void {
    this.cpu.start();
    this.memory.load();
    this.hardDrive.read();
  }

  public stop(): void {
    this.cpu.stop();
    this.memory.unload();
    this.hardDrive.write();
  }
}

// 使用示例
const computer = new Computer();
computer.start();
computer.stop();

在这个示例中,外观模式的优点体现在以下几个方面:

  • 简化了客户端的使用:客户端只需要创建一个 Computer 对象,并调用它的 start() 和 stop() 方法即可完成一系列操作,而不需要了解 CPU、内存、硬盘等多个子系统的具体实现细节。这样可以大大简化客户端的使用,降低了使用门槛。
  • 解耦了客户端和子系统:客户端和子系统之间的解耦使得系统更加灵活,可以方便地进行维护和扩展。如果需要修改子系统的实现,只需要修改子系统类的代码,而不需要修改客户端的代码。这样可以降低系统的耦合度,提高了系统的可维护性和可扩展性。
  • 提高了安全性:外观类可以限制客户端对子系统的访问,从而提高了系统的安全性。在这个示例中,客户端只能通过 Computer 类来访问 CPU、内存、硬盘等子系统,而不能直接访问这些子系统的类。这样可以避免客户端对子系统的误操作,提高了系统的稳定性和安全性。

综上所述,外观模式可以简化客户端的使用,解耦客户端和子系统,提高系统的安全性,从而提高了系统的可维护性、可扩展性和可靠性。

享元模式

享元模式是一种结构型设计模式,它通过共享对象来减少内存的使用和提高性能。享元模式将对象分为两种类型:内部状态和外部状态。内部状态是可以共享的,而外部状态是不可以共享的。通过共享内部状态,可以减少内存的使用,提高系统的性能。

享元模式包含以下角色:

  • 享元工厂(Flyweight Factory):负责创建和管理享元对象。
  • 享元对象(Flyweight):包含内部状态和外部状态,内部状态可以共享,外部状态不可以共享。
  • 客户端(Client):使用享元对象的客户端。

使用场景

  • 当需要创建大量相似的对象时,可以使用享元模式。享元模式可以共享内部状态,从而减少内存的使用,提高系统的性能。
  • 当需要缓存对象时,可以使用享元模式。享元模式可以将对象缓存起来,从而提高系统的效率和性能。
  • 当需要对对象进行细粒度控制时,可以使用享元模式。享元模式可以将对象分为内部状态和外部状态,从而实现对对象的细粒度控制。

代码示例

假设我们需要创建大量的文本编辑器对象,并且这些文本编辑器对象的字体和颜色是相同的,那么我们可以使用享元模式来共享字体和颜色对象,从而减少内存的使用。

// 字体类
class Font {
  private name: string;

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

  public getName(): string {
    return this.name;
  }
}

// 颜色类
class Color {
  private name: string;

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

  public getName(): string {
    return this.name;
  }
}

// 文本编辑器类
class TextEditor {
  private text: string;
  private font: Font;
  private color: Color;

  constructor(text: string, font: Font, color: Color) {
    this.text = text;
    this.font = font;
    this.color = color;
  }

  public draw(): void {
    console.log(`Drawing text "${this.text}" with font ${this.font.getName()} and color ${this.color.getName()}`);
  }
}

// 文本编辑器工厂类
class TextEditorFactory {
  private fonts: { [key: string]: Font } = {};
  private colors: { [key: string]: Color } = {};

  public getTextEditor(text: string, fontName: string, colorName: string): TextEditor {
    let font = this.fonts[fontName];
    if (!font) {
      font = new Font(fontName);
      this.fonts[fontName] = font;
    }
    let color = this.colors[colorName];
    if (!color) {
      color = new Color(colorName);
      this.colors[colorName] = color;
    }
    return new TextEditor(text, font, color);
  }
}

// 使用示例
const factory = new TextEditorFactory();
const editor1 = factory.getTextEditor("Hello, world!", "Arial", "red");
const editor2 = factory.getTextEditor("Goodbye, world!", "Arial", "red");
const editor3 = factory.getTextEditor("Hello, world!", "Times New Roman", "blue");
editor1.draw(); // Drawing text "Hello, world!" with font Arial and color red
editor2.draw(); // Drawing text "Goodbye, world!" with font Arial and color red
editor3.draw(); // Drawing text "Hello, world!" with font Times New Roman and color blue
console.log(editor1 === editor2); // false
console.log(editor1 === editor3); // false

在使用示例中,我们创建了一个 TextEditorFactory 对象,并使用它来创建了三个文本编辑器对象。其中,前两个文本编辑器对象使用了相同的字体对象和颜色对象,而第三个文本编辑器对象使用了不同的字体对象和颜色对象。我们可以看到,前两个文本编辑器对象的字体和颜色是相同的,而第三个文本编辑器对象的字体和颜色是不同的。这样,我们就成功地使用享元模式来共享字体和颜色对象,从而减少内存的使用。

// 数据类
class Data {
  private id: number;
  private value: string;

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

  public getId(): number {
    return this.id;
  }

  public getValue(): string {
    return this.value;
  }
}

// 数据缓存类
class DataCache {
  private cache: { [key: number]: Data } = {};

  public getData(id: number): Data {
    let data = this.cache[id];
    if (!data) {
      // 从远程服务器获取数据
      const value = `value_${id}`;
      data = new Data(id, value);
      this.cache[id] = data;
    }
    return data;
  }
}

// 使用示例
const cache = new DataCache();
const data1 = cache.getData(1);
const data2 = cache.getData(2);
const data3 = cache.getData(1);
console.log(data1 === data2); // false
console.log(data1 === data3); // true

在上面示例中,我们创建了一个 DataCache 对象,并使用它来获取了三个数据对象。其中,前两个数据对象的 ID 是不同的,而第三个数据对象的 ID 与第一个数据对象的 ID 相同。我们可以看到,前两个数据对象是不同的,而第三个数据对象与第一个数据对象是相同的。这样,我们就成功地使用缓存对象来缓存已经获取的数据,从而提高系统的效率和性能。

行为设计模式

状态模式

概念

状态模式是一种行为型设计模式,它允许对象在内部状态发生改变时改变它的行为。状态模式将对象的行为封装在不同的状态类中,使得对象在不同的状态下可以有不同的行为。

在状态模式中,有三个主要的角色:

  • 上下文(Context):维护一个对当前状态对象的引用,并将所有与状态相关的请求委托给它。
  • 状态(State):定义了一个接口,用于封装与上下文的一个特定状态相关的行为。
  • 具体状态(Concrete State):实现状态接口,并提供与状态相关的行为。

状态模式的核心思想是将对象的行为封装在不同的状态类中,使得对象在不同的状态下可以有不同的行为。这种模式可以帮助我们实现复杂的状态机,使得代码更加清晰、易于维护。

应用场景

状态模式主要应用于对象的状态转换和行为随状态改变的场景。当一个对象的行为取决于其内部状态,并且需要在运行时根据状态改变行为时,可以使用状态模式。

以下是一些状态模式的应用场景:

  • 订单状态:当我们需要实现订单状态的转换和相应的行为时,可以使用状态模式。例如,在电商网站中,订单可以有多种状态,如待支付、待发货、已发货、已完成等,每种状态对应着不同的行为,如支付、发货、确认收货等。
  • 线程状态:当我们需要实现线程状态的转换和相应的行为时,可以使用状态模式。例如,在操作系统中,线程可以有多种状态,如就绪、运行、阻塞、挂起等,每种状态对应着不同的行为,如调度、等待、唤醒等。
  • 游戏角色状态:当我们需要实现游戏角色状态的转换和相应的行为时,可以使用状态模式。例如,在角色扮演游戏中,角色可以有多种状态,如正常、中毒、昏迷、死亡等,每种状态对应着不同的行为,如攻击、治疗、复活等。

在状态模式中,我们通常会定义一个状态接口或抽象类来表示状态,多个具体状态类来实现状态的不同行为,以及一个上下文类来维护状态对象并调用其方法。当需要改变状态时,我们可以调用上下文对象的方法来改变状态对象,并在状态对象中实现相应的行为。这样,我们可以将状态的转换和行为的实现分离开来,使得代码更加清晰、易于维护。

代码案例

以下是一个电视机遥控器的状态模式的 JavaScript 实现,包括上下文、状态和具体状态三个角色:

// 上下文
class RemoteControl {
  constructor() {
    this.state = new OffState();
  }

  // 设置状态
  setState(state) {
    this.state = state;
  }

  // 按下电源键
  pressPowerButton() {
    this.state.pressPowerButton();
  }

  // 按下音量键
  pressVolumeButton() {
    this.state.pressVolumeButton();
  }

  // 按下频道键
  pressChannelButton() {
    this.state.pressChannelButton();
  }
}

// 状态
class State {
  pressPowerButton() {}
  pressVolumeButton() {}
  pressChannelButton() {}
}

// 具体状态:开机状态
class OnState extends State {
  pressPowerButton() {
    console.log("关闭电视机");
    this.context.setState(new OffState());
  }

  pressVolumeButton() {
    console.log("调整音量");
  }

  pressChannelButton() {
    console.log("切换频道");
  }
}

// 具体状态:关机状态
class OffState extends State {
  pressPowerButton() {
    console.log("打开电视机");
    this.context.setState(new OnState());
  }

  pressVolumeButton() {
    console.log("电视机已关闭,无法调整音量");
  }

  pressChannelButton() {
    console.log("电视机已关闭,无法切换频道");
  }
}

// 使用示例
const remoteControl = new RemoteControl();

// 按下电源键,打开电视机
remoteControl.pressPowerButton();

// 按下音量键,调整音量
remoteControl.pressVolumeButton();

// 按下频道键,切换频道
remoteControl.pressChannelButton();

// 按下电源键,关闭电视机
remoteControl.pressPowerButton();

// 按下音量键,电视机已关闭,无法调整音量
remoteControl.pressVolumeButton();

// 按下频道键,电视机已关闭,无法切换频道
remoteControl.pressChannelButton();

在这个示例中,我们创建了一个 RemoteControl 类来表示遥控器,一个 State 类来表示遥控器的状态,以及两个具体状态类 OnState 和 OffState。当电视机处于开机状态时,我们将遥控器的状态设置为 OnState;当电视机处于关机状态时,我们将遥控器的状态设置为 OffState。在遥控器的行为发生改变时,我们只需要调用当前状态对象的方法即可。

在使用状态模式时,我们首先需要创建一个遥控器对象,并将其初始状态设置为一个具体状态对象。然后,我们可以通过调用遥控器对象的方法来触发状态的改变。在状态改变时,我们只需要将遥控器对象的状态设置为一个新的具体状态对象即可。

这种设计可以使得遥控器的行为更加清晰、易于维护。同时,如果我们需要添加新的状态,例如静音状态,我们只需要创建一个新的具体状态类即可,而不需要修改遥控器的代码。

备忘录模式

概念

备忘录模式是一种行为型设计模式,它允许在不暴露对象实现细节的情况下保存和恢复对象的内部状态。备忘录模式通常用于需要撤销操作或恢复先前状态的场景。

在备忘录模式中,有三个主要的角色:

  • 发起人(Originator):负责创建一个备忘录,以记录当前时刻它的内部状态,并可以使用备忘录恢复内部状态。
  • 备忘录(Memento):存储发起人的内部状态,并可以防止发起人以外的其他对象访问备忘录。
  • 管理者(Caretaker):负责保存备忘录,并在需要时将其返回给发起人。

备忘录模式的核心思想是将对象的状态保存到备忘录中,以便在需要时可以恢复状态。这种模式可以帮助我们实现撤销和重做操作,或者在某些情况下恢复先前的状态。

应用场景

备忘录模式主要应用于需要保存和恢复对象状态的场景。当我们需要在不破坏对象封装性的前提下,保存对象的内部状态,并在需要时恢复该状态时,可以使用备忘录模式。

以下是一些备忘录模式的应用场景:

  • 撤销操作:当我们需要实现撤销操作时,可以使用备忘录模式来保存对象的历史状态。例如,在文本编辑器中,我们可以使用备忘录模式来保存文本的历史版本,以便用户可以撤销操作并恢复到之前的版本。
  • 数据库事务:当我们需要实现数据库事务时,可以使用备忘录模式来保存数据库操作的历史状态。例如,在银行系统中,我们可以使用备忘录模式来保存用户账户的历史余额,以便在用户进行转账等操作时可以回滚到之前的状态。
  • 游戏存档:当我们需要实现游戏存档功能时,可以使用备忘录模式来保存游戏状态。例如,在角色扮演游戏中,我们可以使用备忘录模式来保存角色的属性、装备、技能等信息,以便在需要时可以恢复到之前的状态。

在备忘录模式中,我们通常会定义一个备忘录类来保存对象的状态,一个发起人类来创建备忘录并恢复状态,以及一个负责人类来管理备忘录。当需要保存对象状态时,我们可以创建一个备忘录对象,并将其保存到负责人对象中;当需要恢复对象状态时,我们可以从负责人对象中获取备忘录对象,并将其传递给发起人对象来恢复状态。

代码示例

以下是备忘录模式的 TypeScript 实现,包括发起人、备忘录和管理者三个角色:

// 发起人
class Originator {
  private state: string;

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

  // 创建备忘录
  createMemento(): Memento {
    return new Memento(this.state);
  }

  // 恢复状态
  restoreMemento(memento: Memento) {
    this.state = memento.getState();
  }

  // 修改状态
  setState(state: string) {
    this.state = state;
  }

  // 显示状态
  getState() {
    return this.state;
  }
}

// 备忘录
class Memento {
  private state: string;

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

  getState() {
    return this.state;
  }
}

// 管理者
class Caretaker {
  private mementos: Memento[] = [];

  // 添加备忘录
  addMemento(memento: Memento) {
    this.mementos.push(memento);
  }

  // 获取最后一个备忘录
  getLastMemento(): Memento {
    return this.mementos.pop();
  }
}

// 使用示例
const originator = new Originator("State 1");
const caretaker = new Caretaker();

// 保存状态
caretaker.addMemento(originator.createMemento());

// 修改状态
originator.setState("State 2");

// 保存状态
caretaker.addMemento(originator.createMemento());

// 修改状态
originator.setState("State 3");

// 恢复状态
originator.restoreMemento(caretaker.getLastMemento());

console.log(originator.getState()); // 输出 "State 2"

在这个示例中,我们创建了一个 Originator 类来表示发起人,一个 Memento 类来表示备忘录,以及一个 Caretaker 类来表示管理者。我们使用这些类来实现状态的保存和恢复。

在使用备忘录模式时,我们首先需要创建一个发起人对象,并将其状态保存到备忘录中。然后,我们可以修改发起人的状态,并再次将其保存到备忘录中。如果需要恢复先前的状态,我们可以从管理者中获取最后一个备忘录,并将其传递给发起人进行恢复。

策略模式

概念

策略模式是一种行为型设计模式,它允许在运行时选择算法的行为。它定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。这使得算法可以独立于使用它们的客户端代码而变化。

策略模式通常由三个部分组成:上下文、策略接口和具体策略。上下文是客户端代码使用的对象,它包含一个策略接口的引用,并在需要时调用策略接口的方法。策略接口定义了一组算法的公共接口,具体策略实现了策略接口,并提供了算法的具体实现。

策略模式的优点包括:

  • 算法可以独立于客户端代码而变化,从而提高代码的灵活性和可维护性。
  • 策略模式可以避免使用大量的条件语句,从而提高代码的可读性和可维护性。
  • 策略模式可以提高代码的复用性,因为不同的算法可以共享相同的接口。

策略模式的缺点包括:

  • 策略模式可能会增加代码的复杂性,因为它需要定义多个策略类。
  • 策略模式可能会增加代码的开销,因为它需要在运行时选择算法。

使用场景

策略模式适用于以下场景:

  • 当一个类有多种行为或算法,并且这些行为或算法可以在运行时动态切换时,可以使用策略模式。
  • 当一个类有多个条件语句,并且这些条件语句会影响类的行为时,可以使用策略模式来避免使用大量的 if else 语句。
  • 当一个类需要根据不同的数据或输入来执行不同的操作时,可以使用策略模式来将这些操作封装成不同的策略类。
  • 当一个类需要在不同的环境中使用不同的算法时,可以使用策略模式来将算法独立于客户端代码而变化。
  • 当一个类需要在运行时动态地选择算法时,可以使用策略模式来提高代码的灵活性和可维护性。

总之,策略模式可以将算法独立于客户端代码而变化,提高代码的灵活性和可维护性。策略模式可以避免使用大量的 if else 语句,提高代码的可读性和可维护性。策略模式可以提高代码的复用性,因为不同的算法可以共享相同的接口。

代码示例

// 定义策略接口
interface DiscountStrategy {
  getDiscount(price: number): number;
}

// 具体策略类:不打折
class NoDiscount implements DiscountStrategy {
  getDiscount(price: number): number {
    return 0;
  }
}

// 具体策略类:打九折
class TenPercentDiscount implements DiscountStrategy {
  getDiscount(price: number): number {
    return price * 0.1;
  }
}

// 具体策略类:打八折
class TwentyPercentDiscount implements DiscountStrategy {
  getDiscount(price: number): number {
    return price * 0.2;
  }
}

// 上下文类
class DiscountCalculator {
  private strategy: DiscountStrategy; // 持有策略接口的引用

  constructor(strategy: DiscountStrategy) {
    this.strategy = strategy; // 通过构造函数注入具体策略类的实例
  }

  setStrategy(strategy: DiscountStrategy) {
    this.strategy = strategy; // 通过 setStrategy 方法动态地切换具体策略类的实例
  }

  calculate(price: number): number {
    const discount = this.strategy.getDiscount(price); // 调用策略接口的方法来计算折扣
    return price - discount; // 返回折扣后的价格
  }
}

// 客户端代码
const calculator = new DiscountCalculator(new NoDiscount()); // 创建上下文对象,并使用具体策略类 NoDiscount 来实现它
console.log(calculator.calculate(100)); // 输出 100

calculator.setStrategy(new TenPercentDiscount()); // 切换具体策略类为 TenPercentDiscount
console.log(calculator.calculate(100)); // 输出 90

calculator.setStrategy(new TwentyPercentDiscount()); // 切换具体策略类为 TwentyPercentDiscount
console.log(calculator.calculate(100)); // 输出 80

观察者模式

概念

观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,它的所有观察者都会收到通知并进行相应的处理。

观察者模式包含以下角色:

  • 抽象主题(Subject):定义了一个抽象接口,用于添加、删除和通知观察者对象。
  • 具体主题(ConcreteSubject):实现了抽象主题接口,维护了一个观察者列表,并在状态发生变化时通知观察者。
  • 抽象观察者(Observer):定义了一个抽象接口,用于接收主题对象的通知。
  • 具体观察者(ConcreteObserver):实现了抽象观察者接口,当接收到主题对象的通知时进行相应的处理。

观察者模式的优点包括:

  • 观察者模式可以实现对象之间的松耦合,主题对象和观察者对象之间没有直接的依赖关系,可以独立地进行扩展和修改。
  • 观察者模式可以实现动态的发布-订阅机制,主题对象可以动态地添加和删除观察者对象,观察者对象也可以动态地订阅和取消订阅主题对象。

观察者模式可以实现广播通信,主题对象可以同时通知多个观察者对象,观察者对象也可以同时接收来自多个主题对象的通知。

使用场景

  • 当一个对象的状态发生变化时,需要通知其他对象进行相应的处理时,可以使用观察者模式。
  • 当一个对象需要在不同的时间点通知其他对象进行相应的处理时,可以使用观察者模式。
  • 当一个对象需要与多个对象进行协作,但又不希望与这些对象直接耦合时,可以使用观察者模式。
  • 当一个对象需要在运行时动态地添加和删除观察者对象时,可以使用观察者模式。
  • 当一个对象需要实现动态的发布-订阅机制时,可以使用观察者模式。

总之,观察者模式是一种非常常用的设计模式,它可以帮助我们实现对象之间的松耦合,提高代码的可维护性和可扩展性。观察者模式可以实现动态的发布-订阅机制,广泛应用于事件驱动的编程模型中。在 React 和 Vue 等前端框架中,也广泛使用了观察者模式来实现组件之间的通信和状态管理。

代码示例

// 定义抽象主题接口
interface Subject {
  registerObserver(observer: Observer): void; // 注册观察者
  removeObserver(observer: Observer): void; // 移除观察者
  notifyObservers(): void; // 通知观察者
}

// 定义抽象观察者接口
interface Observer {
  update(data: any): void; // 更新方法
}

// 定义具体主题类
class ConcreteSubject implements Subject {
  private observers: Observer[] = []; // 观察者列表
  private data: any; // 主题对象的状态

  // 注册观察者
  registerObserver(observer: Observer): void {
    this.observers.push(observer);
  }

  // 移除观察者
  removeObserver(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }

  // 通知观察者
  notifyObservers(): void {
    for (const observer of this.observers) {
      observer.update(this.data);
    }
  }

  // 设置主题对象的状态,并通知观察者
  setData(data: any): void {
    this.data = data;
    this.notifyObservers();
  }
}

// 定义具体观察者类
class ConcreteObserver implements Observer {
  private name: string; // 观察者的名称

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

  // 更新方法
  update(data: any): void {
    console.log(`${this.name} received data: ${data}`);
  }
}

// 创建具体主题对象
const subject = new ConcreteSubject();

// 创建具体观察者对象
const observer1 = new ConcreteObserver('Observer 1');
const observer2 = new ConcreteObserver('Observer 2');

// 注册观察者
subject.registerObserver(observer1);
subject.registerObserver(observer2);

// 设置主题对象的状态,并通知观察者
subject.setData('Hello, world!');

// 移除观察者
subject.removeObserver(observer2);

// 设置主题对象的状态,并通知观察者
subject.setData('Goodbye, world!');