JS设计模式实现(全实例、总结向、超长水文)

97 阅读14分钟

概述

设计模式是一套被广泛使用的解决软件设计问题的经验总结,它们被分为三个不同的类型:创建型、结构型和行为型。以下是所有的设计模式并按照它们所属的类型分类:

单一职责

保证类或方法,只负责某一类工作,有额外行为时,不要怕麻烦,可以新建一个类来管理这些方法,防止在项目迭代过程中出现功能超级丰富且无法维护的巨大的方法或类的情况。也可以提升复用率

开闭原则

类应当是可扩展,而不可修改的,当有一个需求需要修改原有的类时,首先考虑的是在这个类上增加新的属性方法来实现,而非修改它。做到开闭原则的前提是单一职责

里氏替换原则

子类行为与父类完全兼容。重写方法时,要对基类进行扩展,而不是在子类中完全替换它

  • 子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象。子类的参数至少需要是父类参数的相等集合,或包含父类参数集合
  • 子类方法的返回值类型必须与超类方法的返回值类型或是其 子类别相匹配。返回参数至少要是基类返回参数的子集
  • 子类中的方法不应抛出基础方法预期之外的异常类型。错误有可能会穿透所有代码的错误处理逻辑,直接导致代码崩溃
  • 子类不应该加强其前置条件。(不能层层加码)
  • 子类不能削弱其后置条件。(中间商不能赚差价)
  • 超类的不变量必须保留。(不能缺胳膊少腿)
  • 子类不能修改超类中私有成员变量的值 (以下犯上不可取)

接口隔离原则

客户端类不必实现其不需要的行为,可以实现多个接口,但是不要实现多余的接口(知足常乐,各取所需)

依赖倒置原则

高层次的类不应该依赖于低层次的类。 两者都应该依 赖于抽象接口。抽象接口不应依赖于具体实现。具体 实现应该依赖于抽象接口。

创建型模式

创建型模式用于描述对象的创建方式,它们主要关注如何有效地创建对象,以及如何避免不必要的复杂性。

工厂模式(Factory Pattern)

工厂模式是一种创建对象的设计模式,它通过使用工厂方法来抽象对象的创建过程。这种模式通常用于创建多个相似对象,而不需要在每个对象的构造函数中重复相同的代码。(降低成本 ,批量快速生产)

单一职责原则、开闭原则

// 定义一个接口
// 创建一个产品接口,让所有产品都实现它
interface Product {
  name: string;
  price: number;
}
// 定义两个Product接口的具体实现类
// 定义两个类实现接口
class Phone implements Product {
  name = 'Phone';
  price = 1000;
}

class Laptop implements Product {
  name = 'Laptop';
  price = 2000;
}

// 定义工厂类,利用map结构,让type维护Type与Class的关系
class ProductFactory {
  products: {[key: string]: any} = {
    phone: Phone,
    laptop: Laptop
  }

  createProduct(type: string): Product {
    const ProductClass = this.products[type];
    if (!ProductClass) {
      throw new Error('Invalid product type.');
    }
    return new ProductClass();
  }

  // 添加新产品类的方法
  addProduct(name: string, productClass: any) {
    this.products[name] = productClass;
  }
}

// 使用工厂类创建对象
const factory = new ProductFactory();
const phone = factory.createProduct('phone');
const laptop = factory.createProduct('laptop');

// 添加新产品类
class Tablet implements Product {
  name = 'Tablet';
  price = 1500;
}
factory.addProduct('tablet', Tablet);
const tablet = factory.createProduct('tablet');

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 无法确定业务还需添加其他类别,或依赖关系时
  • 想让其他人能够方便的在你的类中进行扩展时 (这里提供了addProduct方法)
  • 对一些类进行复用时,工厂模式可以很方便的将多个类单例化,不重复创建同类型的对象

优点

  • 避免了创建者与具体产品之间的机密耦合
  • 可以将创建产品的代码放在程序的单一位置,从而使的代码更容易维护(单一职责)
  • 无需更新现有客户端代码,可以直接在程序中引入新的产品类型

缺点

  • 代码可能会更加复杂

抽象工厂模式(Abstract Factory Pattern)

单一职责原则、开闭原则

概述

通过对类的工厂抽象使得他的业务对于某一类簇产品的创建,而负责创建某一类产品的实例

重点是工厂模式负责创建产品的实例,抽象工厂则负责创建某一类产品的实例,再由具体工厂方法去创建某一类产品(它的侧重点在于配套批量生产,防止我们拿到的不是同一类产品

// 抽象工厂接口
interface AbstractFactory {
  createProductA(): AbstractProductA;
  createProductB(): AbstractProductB;
}

// 具体工厂实现
class ConcreteFactory1 implements AbstractFactory {
  createProductA() {
    return new ConcreteProductA1();
  }

  createProductB() {
    return new ConcreteProductB1();
  }
}

class ConcreteFactory2 implements AbstractFactory {
  createProductA() {
    return new ConcreteProductA2();
  }

  createProductB() {
    return new ConcreteProductB2();
  }
}

// 抽象产品接口
interface AbstractProductA {
  usefulFunctionA(): string;
}

interface AbstractProductB {
  usefulFunctionB(): string;
}

// 具体产品实现
class ConcreteProductA1 implements AbstractProductA {
  usefulFunctionA() {
    return 'The result of the product A1.';
  }
}

class ConcreteProductA2 implements AbstractProductA {
  usefulFunctionA() {
    return 'The result of the product A2.';
  }
}

class ConcreteProductB1 implements AbstractProductB {
  usefulFunctionB() {
    return 'The result of the product B1.';
  }
}

class ConcreteProductB2 implements AbstractProductB {
  usefulFunctionB() {
    return 'The result of the product B2.';
  }
}

// 客户端代码
class Client {
  factory: AbstractFactory;

  constructor(factory: AbstractFactory) {
    this.factory = factory;
  }

  run() {
    const productA = this.factory.createProductA();
    const productB = this.factory.createProductB();

    console.log(productA.usefulFunctionA());
    console.log(productB.usefulFunctionB());
  }
}

// 使用 ConcreteFactory1
console.log('Client: Testing client code with the first factory type...');
const client1 = new Client(new ConcreteFactory1());
client1.run();

// 使用 ConcreteFactory2
console.log('Client: Testing the same client code with the second factory type...');
const client2 = new Client(new ConcreteFactory2());
client2.run();

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 当代码需要与多个不同系列的相关产品交互,但是无法提前获取相关信息,并需要考虑扩展性时8

优点

  • 保证工厂函数生成的产品都是同一个类蔟

  • 避免客户端代码与产品代码耦合

  • 可以方便的引入新的产品,只需要修改抽象工厂,而不需要改动之前的业务代码

  • 可以将构建产品的代码抽离到一起,集中管理

生成器模式/建造者模式(Builder Pattern)

单一职责原则

将一个负载对象的构建层与其表示层互相分离,同样的构建过程可以采用不同的表示

他关注的是创建的过程,可以在外界干涉创建的结果。(它的侧重点在于规范化生产的过程,将类的初始化作为一条流水线

// 定义产品类
class Product {
  // 定义ABC三个步骤
  private partA: string;
  private partB: string;
  private partC: string;

  setPartA(partA: string) {
    this.partA = partA;
  }

  setPartB(partB: string) {
    this.partB = partB;
  }

  setPartC(partC: string) {
    this.partC = partC;
  }

  s何时使用() {
    console.log(`PartA: ${this.partA}, PartB: ${this.partB}, PartC: ${this.partC}`);
  }
}

// 定义生成器接口
interface Builder {
  buildPartA(): void;
  buildPartB(): void;
  buildPartC(): void;
  getResult(): Product;
}

// 实现具体生成器
// build的主要作用是使得产品与构造方法解耦,
// 这样只要产品存在setPart类方法,都可以使用此构造器来创建
class ConcreteBuilder implements Builder {
  private product: Product = new Product();
  // 生成器中调用产品的三个方法
  buildPartA() {
    this.product.setPartA('PartA');
  }

  buildPartB() {
    this.product.setPartB('PartB');
  }

  buildPartC() {
    this.product.setPartC('PartC');
  }

  getResult() {
    return this.product;
  }
}

// 定义指挥者
class Director {
  private builder: Builder;

  setBuilder(builder: Builder) {
    this.builder = builder;
  }
  // 指挥者在construct函数中,执行builder的几个步骤函数
  construct() {
    this.builder.buildPartA();
    this.builder.buildPartB();
    this.builder.buildPartC();
    return this.builder.getResult();
  }
}

// 使用
const director = new Director();
director.setBuilder(new ConcreteBuilder());
const product = director.construct();
product.s何时使用();

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 当我们想要使用一套构建流程,构建不同的产品时
  • 当一个对象的构建过程过于复杂时

优点

  • 可以分步骤创建对象
  • 不同产品,公共部分可以使用一套制造方法
  • 将复杂的构造代码与业务代码解耦

缺点

  • 代码复杂程度增加

总结

建造者模式的关键就是使用同一套流程,构建不同的对象,组件化思想其实可以算作一种建造者模式,组件的业务逻辑、代码,都是不同的,但都可以通过基本一致的流程渲染到页面中

模版编译->VNODE->创建真实DOM->挂载渲染

单例模式(Singleton Pattern)

单一职责原则

单例模式是一种用于创建只有一个实例的对象的设计模式。这种模式通常用于创建具有全局状态的对象,并确保它只有一个实例。在js中通常用昨晚命名空间对象来实现。

  • module.exports 导出对象就是一个单例
  • export 则不是

// 单例模式示例
class Singleton {
  private static instance: Singleton;
  private constructor() {} // 构造函数私有化,防止外部实例化
  public static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 当你想要保证全局只存在一份某个对象实例时
  • 不希望对象被重复创建、消耗过多资源时

优点

  • 保证一个类只有首次请求时初始化一次,且只有一个实例
  • 全局访问同一个节点
  • 状态共享

存在的问题

  • 多线程语言需要特殊处理,防止每个线程多次创建单例
  • 违反单一职责
  • 单元测试问题,大部分单元测试库都是使用继承来实现模拟对象,但是单例的构造函数被私有化,无法在外部实例化。(ts) juejin.cn/post/702727…

总结

我们在业务开发中使用最多的单例模式就是全局唯一的vue实例对象了。以及在vue2时代,大家喜欢使用new Vue()来创建一个全局的Eventbus,这也是单例,请求库axios我们也是以单例的方式去使用的,以此保证接口与请求的集中管理、返回错误的集中处理。除此之我们熟悉的状态管理库都是单例模式,例如Vuex、Redux、MobX、Pinia。

原型模式(Prototype Pattern)

用原型实例指向创建对象的类,使用于创建新的对象的类共享原型对象的属性以及方法,一般来说,会将可复用的,可共享的,耗时大的从基类中提取,放在他的原型上

  • js使用此模式实现对象实例共享属性或方法js类与继承

interface Prototype {
  clone(): Prototype;
}

class ConcretePrototype1 implements Prototype {
  public clone(): Prototype {
    return Object.create(this);
  }
}

class ConcretePrototype2 implements Prototype {
  public clone(): Prototype {
    return Object.create(this);
  }
}

const prototype1 = new ConcretePrototype1();
const prototype2 = new ConcretePrototype2();

const clone1 = prototype1.clone();
const clone2 = prototype2.clone();

console.log(clone1);
console.log(clone2);

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 快速复制对象,共享对象实例属性与方法,但又不想多次实例化这个类时

优点

  • 克隆对象,并解耦对象与产生此对象的类
  • 使用克隆生成对象,避免反复运行初始化代码

总结

js使用原型模式来实现对象,实例,类之间的关系,它可以方便的获取到任意对象上的属性与方法

结构型模式

结构型模式用于描述如何将对象和类组合成更大的结构,并提供新的功能和接口。它们主要关注对象和类之间的关系以及它们之间的协作方式。常见的结构型模式包括:

适配器模式(Adapter Pattern)

单一职责、开闭原则

将一个类对象的接口,转换成另一外一个接口,以满足用户需求,使对象之间接口的不兼容问题通过适配器得以解决(type-c 转 3.5mm耳机转接头?)


// 定义旧接口
interface OldInterface {
  oldMethod(): string;
}

// 实现旧接口
class OldClass implements OldInterface {
  oldMethod() {
    return "This is the old method.";
  }
}

// 定义新接口
interface NewInterface {
  newMethod(): string;
}

// 实现新接口
class NewClass implements NewInterface {
  newMethod() {
    return "This is the new method.";
  }
}

// 创建适配器
class Adapter implements NewInterface {
  private oldClass: OldInterface;

  constructor(oldClass: OldInterface) {
    this.oldClass = oldClass;
  }

  newMethod() {
    return this.oldClass.oldMethod();
  }
}

// 使用适配器
const oldClass = new OldClass();
const adapter = new Adapter(oldClass);
console.log(adapter.newMethod()); // 输出 "This is the old method."

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 后端返回数据与页面结构不兼容时
  • 框架库/BOM API版本兼容时

优点

  • 可以抽离数据转换代码
  • 当后端数据结构发生改变时,只需要修改适配器,不需要改动客户端代码

总结

最近业务开发中,遇到在WebGL中只显示裁剪范围内视频的需求,原本的VideoNode类是不支持裁剪的,渲染流程是是video source -> webgl,我通过新建一个类继承VideoNode,将流程改为video source -> canvas -> img -> webgl,在canvas中实现了裁剪功能。这时候想起来这应该算适配器模式

在前面的学习过程中,我也通过此模式,实现了一个axios fetch adapter,使用Fetch api替换了原本的

XMLHttpRequest Axios Fetch adapter

桥接模式(Bridge Pattern)

开闭原则,单一职责原则

在系统沿着多个维度变化的同时,又不增加其复杂度,并达到解耦,其关注点在于解耦客户端代码与复杂地层代码,并且在客户端代码发生变化时,不影响底层代码。(它像是翻译官?)

  • 事件触发需要执行的操作,可以提取公共部分,抽象成一个方法,重复使用
  • 关键在于将实现层与抽象层解耦,使得两部分可以独立变化
// 定义抽象和实现接口
interface Abstraction {
  operation(): string;
}

interface Implementation {
  operationImplementation(): string;
}

// 创建实现接口的具体实现
class ConcreteImplementationA implements Implementation {
  operationImplementation(): string {
    return 'ConcreteImplementationA';
  }
}

class ConcreteImplementationB implements Implementation {
  operationImplementation(): string {
    return 'ConcreteImplementationB';
  }
}

// 创建使用实现接口实例的抽象接口的具体实现
class ExtendedAbstractionA implements Abstraction {
  implementation: Implementation;

  constructor(implementation: Implementation) {
    this.implementation = implementation;
  }

  operation(): string {
    const operationImplementation = this.implementation.operationImplementation();
    return 'ExtendedAbstractionA:' + operationImplementation;
  }
}

class ExtendedAbstractionB implements Abstraction {
  implementation: Implementation;

  constructor(implementation: Implementation) {
    this.implementation = implementation;
  }

  operation(): string {
    const operationImplementation = this.implementation.operationImplementation();
    return 'ExtendedAbstractionB:' + operationImplementation;
  }
}

// 使用这些类来展示桥接模式的功能。
const concreteImplementationA = new ConcreteImplementationA();
const extendedAbstractionA = new ExtendedAbstractionA(concreteImplementationA);
extendedAbstractionA.operation(); // 返回'ExtendedAbstractionA: ConcreteImplementationA'

const concreteImplementationB = new ConcreteImplementationB();
const extendedAbstractionB = new ExtendedAbstractionB(concreteImplementationB);
extendedAbstractionB.operation(); // 返回'ExtendedAbstractionB: ConcreteImplementationB'

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 将一个复杂的类,拆分聚合成一个使用相对简单的类

    • 拆分后,后续的修改只需要修改对应层次的类即可,可以避免对整个类改动而造成的副作用和错误
  • 运行时切换不同实现方法,可使用桥接模式

优点

  • 可以创建与平台无关的程序和类
  • 客户端代码和仅和高层抽象部分进行互动,不会接触到平台的详细信息

总结:

桥接模式一般在设计之初就需考虑好,而适配器则是新代码为了兼容旧代码而新建的类,这种设计模式常常出现在跨平台开发的框架中,比如Electron,各种小程序,混合app,都是通过一个基座提供的api与不同的系统交互,调用系统功能,硬件。

它侧重于让互不兼容的两个类,能够进行一定的交互,合作。

组合模式(Composite Pattern)

部分-整体模式,将对象组合成树形结构以表示部分整体的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性,一般通过继承一个父类来保证组合中对象方法属性的统一,方便我们管理与使用(它像是一支军队)

// 假设我们要管理一个公司的各个部门和员工信息

// 首先我们定义所有部分或整体的抽象构件
abstract class Component {
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }
  abstract add(c: Component): void;
  abstract remove(c: Component): void;
  abstract display(depth: number): void;
}

// 然后定义叶子节点(即具体部门或员工)
class Leaf extends Component {
  constructor(name: string) {
    super(name);
  }
  add(c: Component): void {
    console.log("leaf cannot add another component");
  }
  remove(c: Component): void {
    console.log("leaf doesn't have any component to remove");
  }
  display(depth: number): void {
    console.log("-".repeat(depth) + this.name);
  }
}

// 最后定义树枝节点(即整体或部分容器)
class Composite extends Component {
  private children: Component[] = [];
  constructor(name: string) {
    super(name);
  }
  add(c: Component): void {
    this.children.push(c);
  }
  remove(c: Component): void {
    const index = this.children.indexOf(c);
    if (index !== -1) {
      this.children.splice(index, 1);
    }
  }
  display(depth: number): void {
    console.log("-".repeat(depth) + this.name);
    this.children.forEach((child) => {
      child.display(depth + 2);
    });
  }
}

// 使用组合模式来管理公司的部门和员工
// 首先创建各部门、员工
const root = new Composite("公司总部");
const hrDepartment = new Composite("人事部");
const devDepartment = new Composite("技术部");
const salesDepartment = new Composite("销售部");

const hrManager = new Leaf("人事部经理");
const devManager = new Leaf("技术部经理");
const salesManager = new Leaf("销售部经理");

const hrEmployee1 = new Leaf("人事部员工1");
const hrEmployee2 = new Leaf("人事部员工2");
const devEmployee1 = new Leaf("开发工程师1");
const devEmployee2 = new Leaf("开发工程师2");
const salesEmployee1 = new Leaf("销售员工1");
const salesEmployee2 = new Leaf("销售员工2");

// 然后将部门、员工组合成整体
hrDepartment.add(hrEmployee1);
hrDepartment.add(hrEmployee2);
hrDepartment.add(hrManager);

devDepartment.add(devEmployee1);
devDepartment.add(devEmployee2);
devDepartment.add(devManager);

salesDepartment.add(salesEmployee1);
salesDepartment.add(salesEmployee2);
salesDepartment.add(salesManager);

root.add(hrDepartment);
root.add(devDepartment);
root.add(salesDepartment);

// 最后展示公司总部,即整体
root.display(1);

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 实现树状结构时可以使用
  • 希望客户端代码以相同的方式处理简单和复杂的元素,可以使用该模式

优点

  • 利用递归+遍历更方便的使用复杂树结构
  • 无需更改现有代码就可以新增元素,让他成为树的一部分

总结

  • dom结构就是一种组合模式

  • pixi、threejs、zrender.js, konva许多图形库也是这种方式, 主要目的就是为了集中管理

装饰者模式(Decorator Pattern)

装饰者模式是一种用于在不改变对象接口的情况下扩展对象功能的设计模式。在这种模式中,装饰者对象包装原始对象,并添加额外的行为。这种模式通常用于动态地修改对象的行为。(属性不够,装备凑,+999攻击+100%晕眩+999移动+100%暴击)这里就使用了4个装饰器,来增加自身(接口)的实力

/* 这里是 TypeScript 中装饰者设计模式的一个例子,它聚合了多种通知方法。 */

// 定义一个 NewNotification 类接口
interface NewNotification {
  send(message: string): void;
}

// 定义一个实现接口的具体 NewNotification 类
class EmailNotification implements NewNotification {
  send(message: string): void {
    console.log(`发送电子邮件通知:${message}`);
  }
}

// 定义另一个实现接口的具体 NewNotification 类
class SMSNotification implements NewNotification {
  send(message: string): void {
    console.log(`发送 SMS 通知:${message}`);
  }
}

// 定义一个装饰者类,聚合多个 NewNotification 对象
class NotificationAggregator implements NewNotification {
  private notifications: NewNotification[] = [];

  addNotification(notification: NewNotification): void {
    this.notifications.push(notification);
  }

  send(message: string): void {
    for (const notification of this.notifications) {
      notification.send(message);
    }
  }
}

// 创建具体 NewNotification 类的实例
const email = new EmailNotification();
const sms = new SMSNotification();

// 创建一个装饰者类的实例并添加具体 NewNotification 对象
const aggregator = new NotificationAggregator();
aggregator.addNotification(email);
aggregator.addNotification(sms);

// 使用装饰者通过所有的通知方法发送消息
aggregator.send("你好世界!");

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 想将多个类使用统一的方法或类来调用,但继承扩展对象行为的方式难实现,或者不可行时,可以使用此模式
  • 需要在方法或类执行时有些额外行为时

优点

  • 无需创建子类,并且不影响原有的代码
  • 可以在运行时添加或移除某个对象的功能,或某部分代码逻辑

总结

使用装饰器模式时,各个类的代码可能会比较复杂,如果实现行为不受装饰栈顺序影响(当有多个装饰器时,Ts是由内到外执行的)装饰起来会比较困难

vue2,vue3版本都有对应的装饰器(vuex-module-decorators),他可以在不侵入代码的情况下利用提供的装饰器(@LifeCycle,@compoent)等,方便创建组件以及使用生命周期

相对于适配器,它倾向于增强某个接口的功能,适配器则为对象提供不同的接口

外观模式(Facade Pattern)

为一组更加负载的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更加容易,在js中,有时也会用于对底层结构兼容性做统一封装来简化用户使用(它的侧重点在于降低接口的复杂度,屏蔽不需要的功能,是针对其他接口的封装,类似于剪映之于ffmpeg,在这里剪影就可以在一定程度上认为是ffmpeg的外观器)

  • 任何对第三方库的或底层代码出于使用方便为前提而进行的封装都可以称为外观模式,可以让使用者只关注关键部分的功能。比如这个使用Promise封装IndexDB
// 这是 Facade 设计模式的一个示例

// 首先,让我们创建一个代表子系统的类
class SubsystemA {
  public operationA(): string {
    return "子系统A已执行操作A";
  }
}

// 接下来,另一个子系统
class SubsystemB {
  public operationB(): string {
    return "子系统B已执行操作B";
  }
}

// 最后,第三个子系统
class SubsystemC {
  public operationC(): string {
    return "子系统C已执行操作C";
  }
}

// 现在我们将创建一个外观,它将处理子系统之间的交互
class Facade {
  private subsystemA: SubsystemA;
  private subsystemB: SubsystemB;
  private subsystemC: SubsystemC;

  constructor(subsystemA?: SubsystemA, subsystemB?: SubsystemB, subsystemC?: SubsystemC) {
    this.subsystemA = subsystemA || new SubsystemA();
    this.subsystemB = subsystemB || new SubsystemB();
    this.subsystemC = subsystemC || new SubsystemC();
  }
  
  public operation(): string {
    let result = "外观初始化子系统:\n";
    result += this.subsystemA.operationA();
    result += this.subsystemB.operationB();
    result += this.subsystemC.operationC();
    return result;
  }
}

// 现在我们可以使用外观与子系统交互,而无需知道其实现细节
const facade = new Facade();
console.log(facade.operation());

/* 输出:
外观初始化子系统:
子系统A已执行操作A
子系统B已执行操作B
子系统C已执行操作C
*/

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 如果你需要一个指向复杂子系统的直接接口,且只使用了部分复杂接口的功能,此时可以使用外观模式
  • 如果使用时,外观接口也变得复杂起来,此时需要将部分功能抽离为一个新的外观模式

优点

  • 简单化客户端代码对复杂接口的调用

总结

外观模式侧重于创建新接口替换复杂的接口,适配器则是让新接口和旧接口可以无缝衔接使用,任何对复杂api出于简化使用的封装新建一个接口,都可以认作为外观模式

享元模式(Flyweight Pattern)

开闭原则

享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

interface Flyweight {
  operation(uniqueState: any): void; //定义接口
}

class ConcreteFlyweight implements Flyweight {
  private intrinsicState: any; //内部状态

  constructor(intrinsicState: any) {
    this.intrinsicState = intrinsicState; //初始化内部状态
  }

  public operation(uniqueState: any): void { //实现接口方法
    console.log(`ConcreteFlyweight: Displaying intrinsic (${this.intrinsicState}) and unique (${uniqueState}) state.`); //输出内部状态和外部状态
  }
}

class FlyweightFactory {
  private flyweights: {[key: string]: Flyweight} = <any>{}; //定义享元对象

  constructor(initialFlyweights: string[][]) { //初始化享元对象
    for (const state of initialFlyweights) {
      this.flyweights[this.getKey(state)] = new ConcreteFlyweight(state[0]); //创建享元对象
    }
  }

  private getKey(state: string[]): string { //获取享元对象的key
    return state.join('_');
  }

  public getFlyweight(sharedState: string[]): Flyweight { //获取享元对象
    const key = this.getKey(sharedState);

    if (!(key in this.flyweights)) { //如果不存在,则创建新的享元对象
      console.log('FlyweightFactory: Can't find a flyweight, creating new one.');
      this.flyweights[key] = new ConcreteFlyweight(sharedState[0]);
    } else { //如果存在,则重用现有的享元对象
      console.log('FlyweightFactory: Reusing existing flyweight.');
    }

    return this.flyweights[key];
  }

  public listFlyweights(): void { //列出享元对象
    const count = Object.keys(this.flyweights).length;
    console.log(`\nFlyweightFactory: I have ${count} flyweights:`);

    for (const key in this.flyweights) {
      console.log(key);
    }
  }
}

const factory = new FlyweightFactory([ //初始化享元工厂
  ['Chevrolet', 'Camaro2018', 'pink'],
  ['Mercedes Benz', 'C300', 'black'],
  ['Mercedes Benz', 'C500', 'red'],
  ['BMW', 'M5', 'red'],
  ['BMW', 'X6', 'white'],
]);

factory.listFlyweights(); //列出享元对象

function addCarToPoliceDatabase( //添加车辆到警方数据库
  ff: FlyweightFactory, plates: string, owner: string,
  brand: string, model: string, color: string,
) {
  console.log('\nClient: Adding a car to database.');
  const flyweight = ff.getFlyweight([brand, model, color]); //获取享元对象

  flyweight.operation([plates, owner]); //执行操作
}

addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'M5', 'red'); //添加车辆到警方数据库

addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'X1', 'red'); //添加车辆到警方数据库

factory.listFlyweights(); //列出享元对象

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 程序有大量对象,并没有足够内存来存储时使用,

    • 比如上面例子中将Car类转换成一个数组,并且用数组作为join作为key存储在享元工厂中

优点

  • 通过共享数据,节省内存

缺点

  • 节省了内存,必然会牺牲一定的执行速度

总结

享元对象如果只有一个的话,就于单例类似了,但两者并不能混为一谈,单例中的数据可以发生变化,享元则不行

代理模式(Proxy Pattern)

由于一个对象不能直接用另一个对象,所以需要通过代理对象在这两个对象之间起到中介作用

// 代理模式
interface Subject {
  request(): void;
}

class RealSubject implements Subject {
  public request(): void {
    console.log("RealSubject: 处理请求.");
  }
}

class RequetProxy implements Subject {
  private realSubject: RealSubject;

  constructor(realSubject: RealSubject) {
    this.realSubject = realSubject;
  }

  public request(): void {
    if (this.checkAccess()) {
      this.realSubject.request();
      this.logAccess();
    }
  }

  private checkAccess(): boolean {
    console.log("Proxy: 验证访问权限.");
    return true;
  }

  private logAccess(): void {
    console.log("Proxy: 记录访问日志.");
  }
}

function clientCode(subject: Subject) {
  subject.request();
}

console.log("客户端: 直接调用 RealSubject:");
const realSubject = new RealSubject();
clientCode(realSubject);

console.log("");

console.log("客户端: 通过代理调用 RealSubject:");
const proxy = new RequetProxy(realSubject);
clientCode(proxy);

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 记录某个对象的访问/引用

    • 通过引用关系可以判断那个对象正在空闲状态(没有引用指向他)
    • 记录对象的使用日志
    • 可以让一些消耗资源的服务,在被访问的时候再启动
  • 请求结果缓存

    • 请求返回内容过大时,可以在代理层设置缓存,下次请求此数据,代理直接返回缓存中的数据
  • 访问控制

    • 例如各种梯子,就是通过没有被🧱的境外服务器,替你去访问外网,再将获取的内容返回给你

优点

  • 代理透明性,用户无感
  • 可以控制某些对象在访问时再启动,节省资源
  • 即使服务未准备好,代理也可以保证接口能正常访问
  • 开闭原则,可以在不对服务器或客户端进行修改的情况下,创建新代理

缺点

  • 服务被访问时再启动,有可能会造成一些延迟
  • 代码变得复杂

总结

  • 现代脚手架解决跨域问题对后端接口的代理
  • 现代框架响应式原理,对对象get set 以及其他修改对象操作的代理 Proxy
  • jquery时代使用的jsonp, 部分站点的站点访问统计

行为型模式

责任链模式(Chain of Responsibility Pattern)

是一种数据流处理模式,解决请求的发送者与接收者之间的耦合,通过责任链上的多个对象分解请求流程,实现请求在多个对象之间的传递,直到最后一个对象完成请求的处理。

 
// 定义一个抽象类,用于定义处理请求的方法
abstract class Handler {
  protected successor: Handler | null = null;

  public setSuccessor(successor: Handler): void {
    this.successor = successor;
  }

  public abstract handleRequest(request: number): void;
}

// 定义具体的处理类
class ConcreteHandler1 extends Handler {
  public handleRequest(request: number): void {
    if (request >= 0 && request < 10) {
      console.log(`ConcreteHandler1 处理请求 ${request}`);
    } else if (this.successor !== null) {
      this.successor.handleRequest(request);
    }
  }
}

class ConcreteHandler2 extends Handler {
  public handleRequest(request: number): void {
    if (request >= 10 && request < 20) {
      console.log(`ConcreteHandler2 处理请求 ${request}`);
    } else if (this.successor !== null) {
      this.successor.handleRequest(request);
    }
  }
}

class ConcreteHandler3 extends Handler {
  public handleRequest(request: number): void {
    if (request >= 20 && request < 30) {
      console.log(`ConcreteHandler3 处理请求 ${request}`);
    } else if (this.successor !== null) {
      this.successor.handleRequest(request);
    }
  }
}

// 客户端代码
const handler1 = new ConcreteHandler1();
const handler2 = new ConcreteHandler2();
const handler3 = new ConcreteHandler3();

// 建立责任链,一个操作完成后,将自动进行下一个操作
handler1.setSuccessor(handler2);
handler2.setSuccessor(handler3);

const requests = [2, 5, 14, 22, 18, 3, 27, 20];

requests.forEach((request) => {
  handler1.handleRequest(request);
});

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 当程序需要使用不同的方式处理不同种类请求,而且请求类型和顺序预先未知时,可以使用责任链模式
  • 当必须按照顺序执行多个处理者时,可以使用
  • 如果所需处理者及其顺序必须在运行时进行改变,可以使用责任链模式

优点

  • 方便控制顺序
  • 可以对操作的发起类和执行的操作类进行解耦
  • 开闭原则,你可以在不更改现有代码的情况下在程序中处理新增处理者

缺点

  • 责任链中可能有请求未被处理

总结

DOM中的结构就是责任链与组合模式的使用,通过责任链模式实现时间的委托和冒泡,并沿途执行节点的操作,节点又使用发布订阅模式处理具体操作与节点事件的解耦

命令模式(Command Pattern)

将请求转化为一个包含与请求相关的所有信息的对立对象,该转换让我们能根据不同的请求将方法参数化,延迟请求执行,或将其放入队列中,且能实现可撤销操作。

 
// 定义命令接口
interface Command {
  execute(): void;
}

// 定义具体命令类
class ConcreteCommand1 implements Command {
  private receiver: Receiver;

  constructor(receiver: Receiver) {
    this.receiver = receiver;
  }

  execute(): void {
    this.receiver.action1();
  }
}

class ConcreteCommand2 implements Command {
  private receiver: Receiver;

  constructor(receiver: Receiver) {
    this.receiver = receiver;
  }

  execute(): void {
    this.receiver.action2();
  }
}

// 定义接收者类
class Receiver {
  public action1(): void {
    console.log('执行操作1');
  }

  public action2(): void {
    console.log('执行操作2');
  }
}

// 定义调用者类
class Invoker {
  private command: Command;

  public setCommand(command: Command): void {
    this.command = command;
  }

  public executeCommand(): void {
    this.command.execute();
  }
}

// 使用命令模式
const receiver = new Receiver();
const command1 = new ConcreteCommand1(receiver);
const command2 = new ConcreteCommand2(receiver);
const invoker = new Invoker();

invoker.setCommand(command1);
invoker.executeCommand();

invoker.setCommand(command2);
invoker.executeCommand();

UML

暂时无法在飞书文档外展示此内容

上面的代码中,Receiver负责执行具体的操作,Invoker通过command类执行操作

何时使用

  • 如果你需要通过操作来参数化对象可以使用命令模式
  • 将操作放入队列中,延迟操作的执行或远程执行
  • 如果你想实现操作回滚功能,可以使用命令模式

优点

  • 解耦触发和执行操作的类
  • 在不修改已有客户端代码的情况下,在程序中创建新的命令
  • 你可以实现撤销和恢复功能
  • 实现延迟执行操作
  • 命令可以组合

缺点

  • 代码复杂化

总结

命令在发送和请求者之间建立单向连接,我们的css就是一种命令模式的实现,这种模式多见于一些可视化编辑的项目

迭代器模式(Iterator Pattern)

迭代器模式是一种行为设计模式,让你能够在不暴露集合底层实现的表现形式(列表,栈,树等)的情况下,遍历集合中所有的元素(在一个景点中,迭代器模式就是你的向导,他能告诉你下一步往哪走,并能给你一些这个地点介绍)


// 定义一个迭代器接口
interface MyIterator<T> {
  next(): T; // 返回下一个元素
  hasNext(): boolean; // 判断是否还有下一个元素
}

// 实现迭代器接口
class ArrayIterator<T> implements MyIterator<T> {
  private index: number = 0;
  constructor(private array: T[]) {}

  next(): T {
    return this.array[this.index++]; // 返回下一个元素
  }

  hasNext(): boolean {
    return this.index < this.array.length; // 判断是否还有下一个元素
  }
}

const arr = [1, 2, 3, 4, 5];
const iterator = new ArrayIterator(arr);

while (iterator.hasNext()) { // 遍历数组
  console.log(iterator.next()); // 输出数组元素
}

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 当集合背后的数据结构过于复杂而我们希望隐藏这个复杂性时使用
  • 减少迭代的重复代码
  • 能够遍历不同,或者无法预知的数据结构

优点

  • 将体积庞大的遍历算法代码抽取为独立的类,降低遍历复杂性
  • 新的集合类型实现迭代只需要实现迭代器即可,不需要修改现有代码
  • 可以通过迭代器同时遍历多个数据结构
  • 可以暂停遍历

缺点

  • 在能够方便迭代的数据结构中使用此模式,可能只会增加代码的复杂性
  • 对于特殊集合,迭代器可能比直接遍历的效率低(创建方法,返回数据是有性能)

总结

例如在React中的虚拟DOM diff算法中,就使用了迭代器模式来遍历虚拟DOM树。此外,jQuery库中的$.each()方法也是基于迭代器模式实现的。它主要侧重于处理复杂数据结构的遍历,以及减少冗余的用于遍历的代码

中介者模式(Mediator Pattern)

通过中介者对象封装一系列对象的交互,使得对象之间不再相互引用,降低他们之间的耦合,有时中介者对象也可以改变对象之间的交互,中介者一般由观察者模式实现。

 
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();
    }

    if (event === "D") {
      console.log("Mediator reacts on D and triggers following operations:");
      this.component1.doB();
      this.component2.doC();
    }
  }
}

class BaseComponent {
  protected mediator!: Mediator;

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

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");
  }
}

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("");
console.log("Client triggers operation D.");
c2.doD();

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 有大量的对象需要维护互相之间的关系时,将关系抽离出来,利用中介维护
  • 当组件因为过于依赖其他组件而无法在不同场景中复用时,可使用中介模式
  • 为了能够在不同情境下服用一些基本行为,导致需要创建大量组件子类时,可以使用中介模式

优点

  • 将组件间的交流抽取到一处维护
  • 增加新的中介者,不需要修改组件代码
  • 减轻多个组件间的耦合
  • 方便复用组件

缺点

  • 如果所有组件交互都依赖于一个中介,中介可能会变得无法维护

总结

中介模式与观察者模式比较类似,但中介是为了组件间的交互,而观察者则是组件的单向通信,一个组件是作为另一个组件的附属来使用的

备忘录模式(Memento Pattern)

不破坏对象的封装性的前提下,在对象之外,捕获并保存该对象内部的状态,以便日后对象使用或者恢复到以前的某个状态。但是要注意缓存数据量过大时,会对性能造成影响,所以需要使用一些缓存优化策略比如,LRU。或者只记录一些基础数据,复杂数据实时计算

 
/**
 * 备忘录类
 */
class MyMemento {
  private state: string;

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

  /**
   * 获取备忘录状态
   * @returns {string} 备忘录状态
   */
  getState(): string {
    return this.state;
  }
}

/**
 * 发起人类
 */
class MyOriginator {
  private state: string;

  /**
   * 设置状态
   * @param {string} state 状态
   */
  setState(state: string): void {
    this.state = state;
  }

  /**
   * 获取状态
   * @returns {string} 状态
   */
  getState(): string {
    return this.state;
  }

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

  /**
   * 恢复备忘录状态
   * @param {MyMemento} memento 备忘录
   */
  restoreMemento(memento: MyMemento): void {
    this.state = memento.getState();
  }
}

/**
 * 管理者类
 */
class Caretaker {
  private mementoList: MyMemento[] = [];

  /**
   * 添加备忘录
   * @param {MyMemento} memento 备忘录
   */
  addMemento(memento: MyMemento): void {
    this.mementoList.push(memento);
  }

  /**
   * 获取备忘录
   * @param {number} index 索引
   * @returns {MyMemento} 备忘录
   */
  getMemento(index: number): MyMemento {
    return this.mementoList[index];
  }
}

// 使用备忘录
const originator = new MyOriginator();
const caretaker = new Caretaker();

originator.setState('State 1');
originator.setState('State 2');
caretaker.addMemento(originator.createMemento());

originator.setState('State 3');
caretaker.addMemento(originator.createMemento());

originator.setState('State 4');

console.log('Current State:', originator.getState());

originator.restoreMemento(caretaker.getMemento(1));
console.log('Restored State:', originator.getState());

originator.restoreMemento(caretaker.getMemento(0));
console.log('Restored State:', originator.getState());

export {}

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 创建对象快照,回复之前的状态,可以使用备忘录模式
  • 当直接访问对象的成员变量、获取器将导致封装被突破(原发器的状态被泄漏)时可以使用该模式

优点

  • 可以在不破坏对象封装情况的前提下,创建对象的快照
  • 使用负责人维护原发器的状态历史记录来简化原发器的代码(新建一个类用于维护状态)

缺点

  • 内存消耗
  • 负责人必须完整的跟踪原发器的生命周期,这样才能销毁弃用的备忘录
  • 大部分动态语言Js php都不能保证备忘录中的状态一定不被修改

总结

备忘录模式用于记录对象当前的状态,前端可以将状态存储在持久化缓存中来解决一部分空间消耗的问题,一般用于实现撤销重做的功能,或者用于状态管理库中。

观察者模式(Observer Pattern)

关注一对多的对象解耦,中介则是关注点对点的对象解耦。

观察者模式是一种用于在对象之间建立一对多的依赖关系的设计模式。在这种模式中,一个对象(称为主题)维护一个观察者列表,并在主题状态发生变化时通知所有观察者。这种模式通常用于实现事件处理和发布/订阅模式。

interface Observer {
  update: (data: any) => void;
}

interface Subject {
  addObserver: (observer: Observer) => void;
  removeObserver: (observer: Observer) => void;
  notifyObservers: (data: any) => void;
}

class ConcreteSubject implements Subject {
  private observers: Observer[] = [];

  addObserver(observer: Observer) {
    this.observers.push(observer);
  }

  removeObserver(observer: Observer) {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }

  notifyObservers(data: any) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class ConcreteObserver implements Observer {
  update(data: any) {
    console.log(`Received data: ${data}`);
  }
}

const subject = new ConcreteSubject();
const observer1 = new ConcreteObserver();
const observer2 = new ConcreteObserver();

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notifyObservers('Hello World!');

subject.removeObserver(observer2);

subject.notifyObservers('Goodbye World!');

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 一个对象状改变需要通知其他对象时
  • 对象的改变时机是无法确定的时

优点

  • 无需修改发布者代码就能引入新的订阅类
  • 运行时建立对象联系

缺点

  • 通知对象的顺序,可能是随机的,或者按照代码执行顺序决定、比较难预估

总结

axios中就使用了很典型的发布订阅模式来让多个部分的代码监听状态机的变化,vue,rect则用它来实现双线数据绑定,他们是在方法调用时,自动订阅的。

状态模式(State Pattern)

当一个对象内部状态发生改变时,会导致其行为的改变,看起来像是改变了对象

 
// 状态模式
interface State {
  handle(): void;
}

class ConcreteStateA implements State {
  public handle(): void {
    console.log('ConcreteStateA is handling.');
  }
}

class ConcreteStateB implements State {
  public handle(): void {
    console.log('ConcreteStateB is handling.');
  }
}

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();
  }
}

const context = new Context(new ConcreteStateA());
context.request();

context.setState(new ConcreteStateB());
context.request();

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 当对象中用于控制对于某个状态的变化的代码过于复杂时
  • 状态切换对应执行的代码存在许多重复代码时

优点

  • 特定状态的代码放在一个类中,方便维护
  • 无需修改已有状态类和上下文就能新增状态
  • 消除了过于臃肿的状态机条件语句

总结

它跟策略模式很像,但一个策略方法是不知道其他策略的存在的,而状态模式中的方法则知道。

策略模式(Strategy Pattern)

策略模式是一种用于定义一组算法,并将它们封装到独立的类中,使它们可以相互替换的设计模式。在这种模式中,每个算法被封装为一个单独的类,并实现相同的接口,从而允许它们在运行时相互替换。这种模式通常用于实现可插拔的组件和动态选择算法。

  • 优化if判断语句
interface Strategy {
  execute(a: number, b: number): number;
}

class AddStrategy implements Strategy {
  execute(a: number, b: number): number {
    return a + b;
  }
}

class SubtractStrategy implements Strategy {
  execute(a: number, b: number): number {
    return a - b;
  }
}

class Calculator {
  private strategy: Strategy;

  constructor(strategy: Strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy: Strategy) {
    this.strategy = strategy;
  }

  calculate(a: number, b: number): number {
    return this.strategy.execute(a, b);
  }
}

const addStrategy = new AddStrategy();
const subtractStrategy = new SubtractStrategy();

const calculator = new Calculator(addStrategy);
console.log(calculator.calculate(5, 3)); // Output: 8

calculator.setStrategy(subtractStrategy);
console.log(calculator.calculate(5, 3)); // Output: 2

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 想使用对象中不同算法的变体,并希望能在运行时切换算法时
  • 有许多执行某些行为时略有不同的相似的类时
  • 如果算法在上下文逻辑中不是特别重要,使用此模式能将业务与算法实现隔离
  • 类中使用了复杂条件运算并在不同变体中切换时可以使用此模式

优点

  • 运行时切换算法
  • 解耦业务逻辑和算法实现
  • 无需修改上下文代码,就能引入新的策略

缺点

  • 使用者必须知道策略之间的不同

模板方法模式(Template Method Pattern)

模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。

 
abstract class AbstractClass {
  public templateMethod(): void {
    this.baseOperation1();
    this.requiredOperations1();
    this.baseOperation2();
    this.hook1();
    this.requiredOperation2();
    this.baseOperation3();
    this.hook2();
  }

  protected baseOperation1(): void {
    console.log('AbstractClass says: I am doing the bulk of the work');
  }

  protected baseOperation2(): void {
    console.log('AbstractClass says: But I let subclasses override some operations');
  }

  protected baseOperation3(): void {
    console.log('AbstractClass says: But I am doing the bulk of the remaining work');
  }

  protected abstract requiredOperations1(): void;

  protected abstract requiredOperation2(): void;

  protected hook1(): void { }

  protected hook2(): void { }
}

class ConcreteClass1 extends AbstractClass {
  protected requiredOperations1(): void {
    console.log('ConcreteClass1 says: Implemented Operation1');
  }

  protected requiredOperation2(): void {
    console.log('ConcreteClass1 says: Implemented Operation2');
  }
}

class ConcreteClass2 extends AbstractClass {
  protected requiredOperations1(): void {
    console.log('ConcreteClass2 says: Implemented Operation1');
  }

  protected requiredOperation2(): void {
    console.log('ConcreteClass2 says: Implemented Operation2');
  }

  protected hook1(): void {
    console.log('ConcreteClass2 says: Overridden Hook1');
  }
}

function clientCode(abstractClass: AbstractClass) {
  abstractClass.templateMethod();
}

console.log('Same client code can work with different subclasses:');
clientCode(new ConcreteClass1());

console.log('');

console.log('Same client code can work with different subclasses:');
clientCode(new ConcreteClass2());

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 当想扩展某个算法步骤时,而不是整个算法结构时,可使用模版方法
  • 当多个类的算法有一些细微不同时,可以使用此模式

优点

  • 使用模版模式重写步骤代码的公共部分时,可以最小化对其他代码的影响
  • 可以将重复代码提升到一个超类中

缺点

  • 客户端代码会受到此模版方法的限制
  • 子类对原本默认步骤的修改可能会违反替换原则
  • 模版方法越复杂,越难维护

总结

模版方法模式可以用于快速执行子类中共有的方法和步骤,并统一维护

访问者模式(Visitor Pattern)

针对对象结构中的元素,定义在不改变对象的前提下访问结构中元素的新方法

interface Visitor {
  visit(element: MyElement): void;
}

class MyElement {
  accept(visitor: Visitor): void {
    visitor.visit(this);
  }
}

class ConcreteMyElementA extends MyElement {
  operationA(): void {
    console.log('ConcreteMyElementA operationA');
  }
}

class ConcreteMyElementB extends MyElement {
  operationB(): void {
    console.log('ConcreteMyElementB operationB');
  }
}

class ConcreteVisitor1 implements Visitor {
  visit(element: MyElement): void {
    if (element instanceof ConcreteMyElementA) {
      element.operationA();
    } else if (element instanceof ConcreteMyElementB) {
      element.operationB();
    }
  }
}

class ConcreteVisitor2 implements Visitor {
  visit(element: MyElement): void {
    if (element instanceof ConcreteMyElementA) {
      console.log('ConcreteVisitor2 visited ConcreteMyElementA');
    } else if (element instanceof ConcreteMyElementB) {
      console.log('ConcreteVisitor2 visited ConcreteMyElementB');
    }
  }
}

const elements: MyElement[] = [new ConcreteMyElementA(), new ConcreteMyElementB()];

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

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

UML

暂时无法在飞书文档外展示此内容

何时使用

  • 如果你需要对一个复杂对象结构中所有元素执行某些操作时,可以使用访问者模式
  • 非主要行为抽取到访问者类中,让程序的类能更专注工作
  • 当某个行为在一些类中有意义而在其他类中没意义时,可以使用此模式

优点

  • 可以引入不同类对象上的新行为,而不需要对类做出修改

  • 可以将同一行为的不同版本移动到同一个类中

缺点

  • 类的增减,需要修改访问者模式对应方法
  • 两个类之间交互时,可能没有访问私有变量的方法和权限

总结

访问者模式可以在不修改原类的前提下,访问新增类的属性方法。