在软件工程中,设计模式(Design Pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。 ——维基百科

设计原则

SOLID设计原则,SOLID 原则并非单纯的 1 个原则,而是由 5 个设计原则组成的,它们分别是:单一职责原则、开闭原则、里式替换原则、接口隔离原则和依赖反转原则,依次对应 SOLID 中的 S、O、L、I、D 这 5 个英文字母。

  • SRP单一职责原则:指一个类或者模块只负责完成一个职责(或者功能);每一个类,应该要有明确的定义,不要设计大而全的类,要设计粒度小、功能单一的类。

    如果一段代码块(函数 类 模块)负责多个功能,那么当 A 功能需求发生改变的时候改动了代码,就有可能导致 B 功能出现问题,所以一段代码块只应该负责一个职责。

  • OCP开闭原则:指软件实体(模块、类、方法等)应该 对扩展开放、对修改关闭;添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

  • LSP里式替换原则:指子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏;程序中的子类应该可以替换父类出现的任何地方并保持预期不变,所以子类尽量不要改变父类方法的预期行为。

  • ISP接口隔离原则:指接口的调用者不应该被强迫依赖它不需要的接口;当类 A 只需要接口 B 中的部分方法时,因为实现接口需要实现其所有的方法,于是就造成了类 A 多出了部分不需要的代码。这时应该将 B 接口拆分,将类A需要和不需要的方法隔离开来。

  • DIP依赖翻转原则:指高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。

创建型

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

  • 单例模式能保证全局的唯一性,可以减少命名变量
  • 单例模式在一定情况下可以节约内存,减少过多的类生成需要的内存和运行时间
  • 把代码都放在一个类里面维护,实现了高内聚

在普通的情况下我们常见一个类,他有一个say方法,我们需要使用时要先实例化然后调用实例的say方法,这是全局可能会创建多个实例

class Singleton {
  say() {
    console.log('Hi');
  }
}

const s1 = new Singleton();
const s2 = new Singleton();

console.log(s1 === s2); // false

然后我们使用单例模式来改造一下这个类

class Singleton {
  say() {
    console.log('Hi');
  }
  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();

console.log(s1 === s2); // true

通过静态方法来获取全局唯一的实例,所有的方法都由这一个实例来处理。

我们经常使用的Vuex和Redux都是通过单例模式来实现的,如果存在多个store就会导致取值混乱。

工厂模式

用来创建继承同一父类、实现同一接口的子类对象,由给定的类型参数创建具体的对象。其实就是将创建对象的过程单独封装

先来看一下代码吧

enum WorkerTypes {
  CARPENTER,
  ELECTRICIAN,
}

interface FactoryWorker {
  work: () => void
}

class Carpenter implements FactoryWorker {
  work() {
    console.log('锯木头')
  }
}

class Electrician implements FactoryWorker {
  work() {
    console.log('接电线')
  }
}

class Factory {
  static workers = new Map<WorkerTypes, FactoryWorker>([
    [WorkerTypes.CARPENTER, new Carpenter()],
    [WorkerTypes.ELECTRICIAN, new Electrician()],
  ])

  static getWorkers(type: WorkerTypes): FactoryWorker {
    return Factory.workers.get(type)
  }
}

Factory.getWorkers(WorkerTypes.CARPENTER).work() // 锯木头
Factory.getWorkers(WorkerTypes.ELECTRICIAN).work() // 接电线

我们在使用时不需要new对象,所有的new操作都交给工厂负责,我们只需要传参即可,这种形式在定义的过程可能会麻烦,但是在使用的时候是真的舒服。

上面这种工厂每种类型只会有一个实例,还有一种工厂每次调用都会创建一个对象

function User(name , age, career, work) {
    this.name = name
    this.age = age
    this.career = career 
    this.work = work
}

function Factory(name, age, career) {
    let work
    switch(career) {
        case 'coder':
            work = () => console.log('写代码')
            break
        case 'product manager':
            work = () => console.log('订会议室')
            break
        case 'boss':
            work = () => console.log('喝茶')
    return new User(name, age, career, work)
}

结构型

适配器模式

将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

用于补救设计上的缺陷,将不兼容的接口变得兼容;封装有缺陷的接口设计;统一多个类的接口设计;替换依赖的外部系统;兼容老版本接口。

  • Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。
  • Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对 Adaptee 和 Target 进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承 Target 并关联一个 Adaptee 对象使二者产生联系。
// 目标类接口
interface ITarget {
    func1: () => void;
    func2: () => void;
}

// 原始类不符合目标类的约束
class Origin {
    funcA() {
        console.log('function A')
    }

    funcB() {
        console.log('function B')
    }
}

// 创建适配器来兼容原始类
class Adapter implements ITarget {
    origin = new Origin()

    func1(): void {
        this.origin.funcA()
    }

    func2(): void {
        this.origin.funcB()
    }
}

const adapter = new Adapter()
adapter.func1() // function A
adapter.func2() // function B

代理模式

是指为一个原对象找一个代理对象,以便对原对象进行访问。即在访问者与目标对象之间加一层代理,通过代理做授权和控制。

通常用来给原类添加非功能性需求,为了将代码与原业务解耦;业务系统的非功能性需求开发:监控、统计、鉴权、限流、日志、缓存。

ES6中的Proxy就是这一模式的实现;DOM中我们经常被问的事件代理;还有我们经常说的“科学上网”,也是代理模式,通过一台代理服务器当做跳板来请求被屏蔽的网站。

image-20211027154955299

代理模式包含如下角色:

  • Subject(抽象主题角色):声明了目标对象和代理对象的共同接口,这样一来在任何可以使用目标对象的地方都可以使用代理对象。
  • Proxy(代理主题角色):也称为委托角色或者被代理角色。定义了代理对象所代表的目标对象。
  • RealSubject(真实主题角色):也叫委托类、代理类。代理对象内部含有目标对象的引用,从而可以在任何时候操作目标对象;代理对象提供一个与目标对象相同的接口,以便可以在任何时候替代目标对象。代理对象通常在客户端调用传递给目标对象之前或之后,执行某个操作,而不是单纯地将调用传递给目标对象。
interface IUser {
    login: () => void;
}

class User implements IUser {
    login(): void {
        console.log('login')
    }
}

class UserProxy implements IUser {
    user = new User();

    login(): void {
        console.log('before login')
        this.user.login();
        console.log('after login')
    }
}

const user = new UserProxy()
user.login()
// before login
// login
// after login

此外还可以使用代理实现缓存的功能,也叫做缓存代理

class Computer {
  static addAll(...args: number[]) {
    console.log('进行了一次新计算')
    let result = 0
    const len = args.length
    for (let i = 0; i < len; i++) {
      result += args[i]
    }
    return result
  }
}

class ComputerProxy {
  private static cache: Record<string, number> = {};

  static sum(...argsList: number[]): number {
    let args = Array.prototype.join.call(arguments, ',');

    if (args in ComputerProxy.cache) {
      return ComputerProxy.cache[args]
    }
    return (ComputerProxy.cache[args] = Computer.addAll(...argsList))
  }
}

console.log(ComputerProxy.sum(1, 2, 3))
console.log(ComputerProxy.sum(1, 2, 3))

从结果可以看到只进行了一次计算,第二次读取值直接从缓存中读取了

image-20211027163655434

装饰器模式

动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。

装饰器类是对原始功能的增强;装饰器类和原始类继承同样的父类,这样我们可以对原始类嵌套多个装饰器类;主要解决继承关系过于复杂的问题,通过组合来替代继承。

装饰模式包含如下角色:

  • Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
  • ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
  • Decorator(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
  • ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。
interface IDecorator {
  event: () => void;
}

class Target implements IDecorator {
  event(): void {
    console.log('target event');
  }
}

class Decorator implements IDecorator {
  private target: Target;

  constructor(target: Target) {
    this.target = target;
  }

  event(): void {
    this.before();
    this.target.event();
    this.after();
  }

  before(): void {
    console.log('before event');
  }

  after(): void {
    console.log('after event');
  }
}

const target = new Target();
const decorator = new Decorator(target);

decorator.event()
// before event
// target event
// after event

桥接模式

将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。

将抽象和实现解耦,让它们可以独立变化;一个类存在多个独立变化的维度,我们通过组合的方式,让多个维度可以独立进行扩展。

桥接模式包含如下角色:

  • Abstraction(抽象类):定义抽象接口,拥有一个 Implementor 类型的对象引用
  • RefinedAbstraction(扩充抽象类):扩展 Abstraction 中的接口定义
  • Implementor(实现类接口):是具体实现的接口,Implementor 和 RefinedAbstraction 接口并不一定完全一致,实际上这两个接口可以完全不一样 Implementor 提供具体操作方法,而 Abstraction 提供更高层次的调用
  • ConcreteImplementor(具体实现类):实现 Implementor 接口,给出具体实现

理解桥接模式,重点需要理解如何将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化。

  • 抽象化:抽象化就是忽略一些信息,把不同的实体当作同样的实体对待。在面向对象中,将对象的共同性质抽取出来形成类的过程即为抽象化的过程。
  • 实现化:针对抽象化给出的具体实现,就是实现化,抽象化与实现化是一对互逆的概念,实现化产生的对象比抽象化更具体,是对抽象化事物的进一步具体化的产物。
  • 脱耦:脱耦就是将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联,将两个角色之间的继承关系改为关联关系。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。

手机的品牌有很多,市场上的手机新品也有很多,它们之间的组合就可以使用桥接模式来表达

interface ICPU {
  work: () => void;
}

abstract class AbstractPhone {
  public cpu: ICPU | null;

  constructor() {
    this.cpu = null
  }

  abstract installCPU(cpu: ICPU): void;

  abstract open(): void;
}

class Qualcomm implements ICPU {
  work(): void {
    console.log('高通晓龙火力全开!')
  }
}

class MediaTek implements ICPU {
  work(): void {
    console.log('天玑助你五杀超神')
  }
}

class MI extends AbstractPhone {
  installCPU(cpu: ICPU): void {
    this.cpu = cpu
  }

  open(): void {
    console.log("为发烧而生")
    this.cpu?.work()
  }
}

class Oppo extends AbstractPhone {
  installCPU(cpu: ICPU): void {
    this.cpu = cpu
  }

  open(): void {
    console.log("科技雕琢生活")
    this.cpu?.work()
  }
}

const phone1 = new MI()
phone1.installCPU(new Qualcomm())
phone1.open()
const phone2 = new MI()
phone2.installCPU(new MediaTek())
phone2.open()
const phone3 = new Oppo()
phone3.installCPU(new Qualcomm())
phone3.open()
const phone4 = new Oppo()
phone4.installCPU(new MediaTek())
phone4.open()

image-20211028192207396

如果后续再有其他的组合类型继续添加即可,不会对之前的代码产生任何影响

组合模式

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

组合模式主要有三种角色:

  • 抽象组件(Component):抽象类,主要定义了参与组合的对象的公共接口
  • 子对象(Leaf):组成组合对象的最基本对象
  • 组合对象(Composite):由子对象组合起来的复杂对象
abstract class AbstractComponent {
  name: string;
  description: string;

  constructor(name: string, description: string) {
    this.name = name
    this.description = description
  }

  abstract show(): void;

  add(component: AbstractComponent): void {
    throw new Error("Unsupported Error")
  }

  remove(component: AbstractComponent): void {
    throw new Error("Unsupported Error")
  }
}

class Company extends AbstractComponent {
  components = new Set<AbstractComponent>()

  // constructor(name: string, description: string) {
  //   super(name, description)
  // }

  show(): void {
    console.log("名称: ", this.name, "描述: ", this.description)
    console.log("子部门")
    for (let value of this.components.values()) {
      console.log("  -名称: ", value.name, "描述: ", value.description)
    }
  }

  add(component: AbstractComponent): void {
    this.components.add(component)
  }

  remove(component: AbstractComponent) {
    this.components.delete(component)
  }
}

class Department extends AbstractComponent {
  show(): void {
    console.log("名称: ", this.name, "描述: ", this.description)
  }

}

const company = new Company('阿里巴巴', '国内顶尖的电商平台');
const rookie = new Department('菜鸟网络', '阿里旗下快递综合服务平台');
const alipay = new Department('蚂蚁金服', '阿里旗下互联网支付平台');

company.add(rookie)
company.add(alipay)
company.show()

rookie.show()
rookie.add(alipay)

在抽象组件中定义抽象方法show,以及add和remove方法,这两个方法默认抛出错误,在组合对象中重写这两个方法,子对象不处理,子对象调用这两个方法时会抛出错误

image-20211029085930002

享元模式

运用共享技术有效地支持大量细粒度对象的复用,前提是享元对象是不可变对象(初始化之后不再改变)。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。享元模式结构较为复杂,一般结合工厂模式一起使用。

享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。

享元模式的核心在于享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。

享元模式以共享的方式高效地支持大量的细粒度对象,享元对象能做到共享的关键是区分内部状态(Internal State)和外部状态(External State)。

  • 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享。
  • 外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。

常见的数据库连接池就是一个享元模式的经典应用。

享元模式包含如下角色:

  • Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
  • ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
  • UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂一共一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。
enum ChessColor {
  WHITE_CHESS = 0,
  BLACK_CHESS = 1
}

abstract class Chess {
  protected x: number = 0;
  protected y: number = 0;

  abstract play(x: number, y: number): void;
}

class WhiteChess extends Chess {
  play(x: number, y: number): void {
    this.x = x;
    this.y = y;
    console.log(`"○落子-(${this.x},${this.y})`)
  }
}

class BlackChess extends Chess {
  play(x: number, y: number): void {
    this.x = x;
    this.y = y;
    console.log(`"●落子-(${this.x},${this.y})`)
  }
}

class ChessFactory {
  static whiteChess: Chess;
  static blackChess: Chess;

  static getChess(chessman: ChessColor): Chess {
    if (chessman === ChessColor.WHITE_CHESS) {
      if (!ChessFactory.whiteChess) {
        console.log("创建白棋对象")
        ChessFactory.whiteChess = new WhiteChess()
      }
      return ChessFactory.whiteChess
    } else {
      if (!ChessFactory.blackChess) {
        console.log("创建黑棋对象")
        ChessFactory.blackChess = new BlackChess()
      }
      return ChessFactory.blackChess
    }
  }
}

for (let i = 0; i < 10; i++) {
  const chessman = ChessFactory.getChess(i % 2)
  chessman.play(i, Math.floor(Math.random() * 10))
}

可以看见结果中只创建了黑棋和白棋对象各一个

image-20211029102202773

行为型

观察者模式

熟悉Vue的应该对观察者模式不陌生,Vue的双向绑定就是基于观察者模式来实现的

观察者模式定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

注意区分发布订阅模式:发布订阅模式有发布订阅调度中心(中间商),观察者模式没有!!!

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

  • Subject(目标):知道它的通知对象,事件发生后会通知所有它知道的对象,提供添加删除观察者的接口。
  • ConcreteSubject(具体目标):被观察者具体的实例,存储观察者感兴趣的状态。
  • Observer(观察者):提供通知后的更新事件。
  • ConcreteObserver(具体观察者):被观察者具体的实例,存储观察者感兴趣的状态。
class Subject {
  private observers: Observer[];

  constructor() {
    this.observers = []
  }

  attach(observer: Observer): Subject {
    this.observers.push(observer)
    return this
  }

  notify(): void {
    this.observers.forEach(observer => observer.update())
  }
}

class Observer {
  private readonly name: string;

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

  update(): void {
    console.log(this.name + '触发更新')
  }
}

const subject = new Subject();
const observer1 = new Observer('observer1');
const observer2 = new Observer('observer2');

subject.attach(observer1).attach(observer2)
subject.notify()
// observer1触发更新
// observer2触发更新

模板模式

定义了一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

模板模式包含如下角色:

  • 抽象父类:封装子类的算法框架,包括实现一些公用方法以及封装在子类中所有方法的执行顺序
  • 实现子类:通过集成这个抽象类,也继承了整个算法,并且可以选择重写父类的方法 假如我们有许多平行的类,各个类之间有许多相同的行为,也有部分不同的行为。如果各位都定义自己所有的行为,那么会出现很多重复的方法。此时可以将相同的行为搬移到另外一个单一的地方,模板方法模式就是为了解决这个问题。在模板方法模式中,子类中相同的行为被移动到了父类中,而将不同的部分留待子类来实现。
abstract class Drive {
  initCar(): void {
    console.log('有一辆车')
  }

  abstract openDoor(): void;

  fire(): void {
    console.log('点火')
  }

  start(): void {
    this.initCar()
    this.openDoor()
    this.fire()
  }
}

class MainlandDrive extends Drive {
  openDoor(): void {
    console.log('从左门进入')
  }
}

class HongKongDrive extends Drive {
  openDoor(): void {
    console.log('从右门进入')
  }
}

const m = new MainlandDrive()
m.start()
const h = new HongKongDrive()
h.start()
// 有一辆车
// 从左门进入
// 点火
// 有一辆车
// 从右门进入
// 点火

状态模式

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。

通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。

状态模式包含如下角色:

  • Context(环境类):定义客户端所感兴趣的接口,并且保留一个具体状态类的实例。这个具体状态类的实例给出此环境对象的现有状态。
  • State(抽象状态类):定义一个接口,用以封装环境(Context)对象的一个特定的状态所对应的行为。
  • ConcreteState(具体状态类):每一个具体状态类都实现了环境(Context)的一个状态所对应的行为。
abstract class CookModel {
  protected cooker: RiceCooker;

  constructor(cooker: RiceCooker) {
    this.cooker = cooker
  }

  abstract press(): void;
}

class Braised extends CookModel {
  press(): void {
    console.log('打开煮粥模式')
    this.cooker.setModel(this.cooker.porridge)
  }
}

class Porridge extends CookModel {
  press(): void {
    console.log('关闭电饭煲')
    this.cooker.setModel(this.cooker.off)
  }
}

class Off extends CookModel {
  press(): void {
    console.log('打开煮饭模式')
    this.cooker.setModel(this.cooker.braised)
  }
}

class RiceCooker {
  braised: Braised;
  porridge: Porridge;
  off: Off;
  private currentModel: CookModel;

  constructor() {
    this.braised = new Braised(this)
    this.porridge = new Porridge(this)
    this.off = new Off(this)
    this.currentModel = this.off
  }

  setModel(model: CookModel): void {
    this.currentModel = model
  }

  press(): void {
    this.currentModel.press()
  }
}

const cooker = new RiceCooker()
cooker.press()
cooker.press()
cooker.press()
cooker.press()

image-20211031135901141

策略模式

定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。

可以用来避免冗长的 if-else 或 switch 分支判断。

策略模式包含如下角色:

  • Context(环境类):持有一个 Strategy 类的引用,用一个 ConcreteStrategy 对象来配置。
  • Strategy(抽象策略类):定义所有支持的算法的公共接口。通常是以一个接口或抽象来实现,Context 使用这个接口来调用其 ConcreteStrategy 定义的算法。
  • ConcreteStrategy(具体策略类):以 Strategy 接口实现某具体算法。
abstract class Strategy {
  abstract strategyMethod(): void;
}

class StrategyA extends Strategy {
  strategyMethod(): void {
    console.log("A策略被访问")
  }
}

class StrategyB extends Strategy {
  strategyMethod(): void {
    console.log("B策略被访问")
  }
}

class Context {
  strategy: Strategy

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

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

  strategyMethod(): void {
    this.strategy.strategyMethod()
  }
}

const context = new Context(new StrategyA())
context.strategyMethod() // A策略被访问
context.setStrategy(new StrategyB())
context.strategyMethod() // B策略被访问

职责链模式

解决请求的发送者和请求的接受者之间的耦合,通过职责链上的多个对象对分解请求流程,实现请求在多个对象之间的传递,直到有对象处理它为止。

多个处理器 ABC 依次处理同一个请求,形成一个链条,当某个处理器能处理这个请求,就不会继续传递给后续处理器了。常用于过滤器、拦截器、处理器。

职责链模式包含如下角色:

  • 抽象处理者(Handler):定义一个处理请求的抽象类。如果需要,可以定义一个方法以设定返回对下家的引用。
  • 具体处理者(ConcreteHandler):具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家。

以看病过程为例,用代码实现一下职责链模式

// 病因
enum Disease {
  FRACTURE = '骨折',
  SHORTSIGHTED = '近视',
  WISDOM_TEETH = '智齿',
  STOMACHACHE = '胃疼'
}

// 医生抽象类
abstract class Doctor {
  private nextDoctor: Doctor | null;

  constructor() {
    this.nextDoctor = null
  }

  treat(disease: Disease): void {
    if (this.getDisease() === disease) {
      this.cure(disease)
    } else {
      if (this.nextDoctor !== null) {
        console.log('寻找下一位大夫')
        this.nextDoctor.treat(disease)
      } else {
        console.log('无法医治')
      }
    }
  }

  setNextDoctor(doctor: Doctor): void {
    this.nextDoctor = doctor;
  }

  // 自己能够治愈的疾病
  abstract cure(disease: Disease): void;

  // 获取自己能处理的类型
  abstract getDisease(): Disease;
}

// 骨科大夫
class OrthopedicsDoctor extends Doctor {
  cure(disease: Disease): void {
    console.log(disease + '已经治愈')
  }

  getDisease(): Disease {
    return Disease.FRACTURE;
  }
}

// 眼科大夫
class OphthalmologyDoctor extends Doctor {
  cure(disease: Disease): void {
    console.log(disease + '已经治愈')
  }

  getDisease(): Disease {
    return Disease.SHORTSIGHTED;
  }
}

// 牙科大夫
class DentistryDoctor extends Doctor {
  cure(disease: Disease): void {
    console.log(disease + '已经治愈')
  }

  getDisease(): Disease {
    return Disease.WISDOM_TEETH;
  }
}

const doctor1 = new OrthopedicsDoctor()
const doctor2 = new OphthalmologyDoctor()
const doctor3 = new DentistryDoctor()

doctor1.setNextDoctor(doctor2)
doctor2.setNextDoctor(doctor3)

doctor1.treat(Disease.WISDOM_TEETH)
doctor1.treat(Disease.STOMACHACHE)

image-20211031225901256

命令模式

将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。

命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等。将命令的发起者和执行者解耦。

命令模式包含如下角色:

  • Invoker:调用者角色;
  • Command:命令角色,需要执行的所有命令都在这里,可以是接口或抽象类;
  • Receiver:接收者角色,知道如何实施和执行一个请求相关的操作;
  • ConcreteCommand:将一个接受者对象与一个动作绑定,调用接收者相应的的操作,实现execute。
interface Command {
  execute: () => void;
}

class MySQLCommand implements Command {
  execute(): void {
    console.log('启动MySQL')
  }
}

class NodeCommand implements Command {
  execute(): void {
    console.log('启动Node')
  }
}

class CloseCommand implements Command {
  execute(): void {
    console.log('退出终端')
  }
}

class Terminal {
  private commandList: Command[];

  constructor() {
    this.commandList = []
  }

  addCommand(command: Command): void {
    this.commandList.push(command);
  }

  execute(): void {
    this.commandList.forEach(command => command.execute())
  }
}

const terminal = new Terminal()
terminal.addCommand(new MySQLCommand())
terminal.addCommand(new NodeCommand())
terminal.addCommand(new CloseCommand())
terminal.execute()
// 启动MySQL
// 启动Node
// 退出终端

访问者模式

访问者模式主要适用于以下情况:

  • 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而且需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
  • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。

访问者模式包含如下角色:

  • Visitor:接口或者抽象类,它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法数理论上来讲与元素个数是一样的,因此,访问者模式要求元素的类族要稳定,如果经常添加、移除元素类,必然会导致频繁地修改Visitor接口,如果这样则不适合使用访问者模式。
  • ConcreteVisitor1、ConcreteVisitor2:具体的访问类,它需要给出对每一个元素类访问时所产生的具体行为。
  • Element:元素接口或者抽象类,它定义了一个接受访问者的方法(Accept),其意义是指每一个元素都要可以被访问者访问。
  • ConcreteElementA、ConcreteElementB:具体的元素类,它提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • ObjectStructure:定义当中所说的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素供访问者访问。
abstract class Visitor {
  name: string;

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

  visit(element: TargetElement): void {
    console.log(`${element.name}被${this.name}访问`)
  }
}

abstract class TargetElement {
  name: string;

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

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

class Visitor1 extends Visitor {
  constructor() {
    super('visitor1');
  }
}

class TargetElement1 extends TargetElement {
  constructor() {
    super('target1');
  }
}

class Container {
  elementList: Set<TargetElement>;

  constructor() {
    this.elementList = new Set()
  }

  add(element: TargetElement): void {
    this.elementList.add(element)
  }

  remove(element: TargetElement): void {
    this.elementList.delete(element)
  }

  accept(visitor: Visitor): void {
    this.elementList.forEach(element => element.accept(visitor))
  }
}

const container = new Container()
container.add(new TargetElement1())
const visitor = new Visitor1()
container.accept(visitor) // target1被visitor1访问

中介者模式

用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

中介者模式包含如下角色:

  • Mediator是抽象中介者,定义了同事对象到中介者对象的接口;
  • Colleague是抽象同事类;
  • ConcreteMediator是具体中介者对象,实现抽象类的方法,它需要知道所有具体同事类,并从具体同事接收消息,向具体同事对象发出命令;
  • ConcreteColleague是具体同事类,每个具体同事只知道自己的行为,而不了解其它同事类的情况,但它们却都认识中介者对象。
abstract class Colleague {
  protected mediator: Mediator;

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

  send(message: string): void {
    this.mediator.send(message, this)
  }

  abstract notify(message: string): void;
}

abstract class Mediator {
  abstract send(message: string, colleague: Colleague): void;
}

class ConcreteMediator extends Mediator {
  colleague1: ConcreteColleague1 | null;
  colleague2: ConcreteColleague2 | null;

  constructor() {
    super();
    this.colleague1 = null;
    this.colleague2 = null;
  }

  setColleague1(colleague1: ConcreteColleague1): void {
    this.colleague1 = colleague1
  }

  setColleague2(colleague2: ConcreteColleague2): void {
    this.colleague2 = colleague2
  }

  send(message: string, colleague: Colleague): void {
    if (colleague === this.colleague1) {
      this.colleague2?.notify(message)
    } else {
      this.colleague1?.notify(message)
    }
  }
}

class ConcreteColleague1 extends Colleague {
  notify(message: string): void {
    console.log('同事1得到消息: ', message)
  }
}

class ConcreteColleague2 extends Colleague {
  notify(message: string): void {
    console.log('同事2得到消息: ', message)
  }
}

const mediator = new ConcreteMediator()
const colleague1 = new ConcreteColleague1(mediator)
const colleague2 = new ConcreteColleague2(mediator)
mediator.setColleague1(colleague1)
mediator.setColleague2(colleague2)
colleague1.send('Hi')
colleague2.send('hello')

image-20211101211102142

备忘录模式

在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token。

备忘录模式常见的场景如:浏览器回退、数据库还原/备份、git版本管理……

备忘录模式包含如下角色:

  • Originator(原发器):它是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器。
  • Memento(备忘录):存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同。
  • Caretaker(负责人):负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。
interface IProgrammer {
  age: number
  username: string
  area: string
}

class Programmer implements IProgrammer {
  age: number
  username: string
  area: string

  constructor(age: number, username: string, area: string) {
    this.age = age
    this.username = username
    this.area = area
  }

  // 创建一个快照
  createSnapshot(): IProgrammer {
    return {
      age: this.age,
      username: this.username,
      area: this.area
    }
  }

  // 通过快照恢复对象状态
  restoreSnapshot(snapshot: IProgrammer) {
    this.age = snapshot.age
    this.username = snapshot.username
    this.area = snapshot.area
  }
}

const p = new Programmer(13, 'king', 'CN');
const copy = p.createSnapshot();
console.log(JSON.stringify(p));
p.age = 16
console.log(JSON.stringify(p));
p.restoreSnapshot(copy)
console.log(JSON.stringify(p));

image-20211101213224102

迭代器模式

迭代器模式是一种对象行为型模式,提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。

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

  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

JavaScript API中的for…in遍历时就用到了迭代器

interface AbstractIterator {
  next(): any;

  hasNext(): boolean;

  remove(): boolean;
}

class ConcreteIterator implements AbstractIterator {
  public cursor: number = 0;
  private readonly list: any[];

  constructor(array: any[]) {
    this.list = array;
  }

  public next(): any {
    return this.hasNext() ? this.list[this.cursor++] : null;
  }

  public hasNext(): boolean {
    return this.cursor < this.list.length;
  }

  public remove(): boolean {
    this.list.splice(this.cursor--, 1);
    return true;
  }
}

interface Aggregate {
  add(value: any): void;

  remove(value: any): void;

  createIterator(): AbstractIterator;
}

class ConcreteAggregate implements Aggregate {
  // 容纳对象的容器
  private readonly list: any[];

  constructor() {
    this.list = [];
  }

  add(value: any): void {
    this.list.push(value)
  }

  remove(value: any): void {
    const index = this.list.findIndex((listValue) => {
      return value === listValue;
    });
    this.list.splice(index, 1);
  }

  createIterator(): AbstractIterator {
    return new ConcreteIterator(this.list);
  }
}

const aggregate = new ConcreteAggregate();
aggregate.add('step 1')
aggregate.add('step 2')
aggregate.add('step 3')
const iterator = aggregate.createIterator()
while (iterator.hasNext()) {
  console.log(iterator.next())
}

image-20211101214357687

解释器模式

给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。

解释器模式的结构与组合模式相似,不过其包含的组成元素比组合模式多,而且组合模式是对象结构型模式,而解释器模式是类行为型模式。

解释器模式包含以下主要角色。

  • 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
  • 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
  • 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
  • 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
  • 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
abstract class Expression {
  public abstract interpreter(str: string): boolean;
}

class TerminalExpression extends Expression {
  private readonly literal: string;

  constructor(str: string) {
    super();
    this.literal = str;
  }

  public interpreter(str: string): boolean {
    for (let charVal of str) {
      if (charVal === this.literal) {
        return true;
      }
    }

    return false;
  }
}

class AndExpression extends Expression {
  private expression1: Expression;
  private expression2: Expression;

  constructor(expression1: Expression, expression2: Expression) {
    super();
    this.expression1 = expression1;
    this.expression2 = expression2;
  }

  public interpreter(str: string): boolean {
    return this.expression1.interpreter(str) && this.expression2.interpreter(str);
  }
}

class OrExpression extends Expression {
  private expression1: Expression;
  private expression2: Expression;

  constructor(expression1: Expression, expression2: Expression) {
    super();
    this.expression1 = expression1;
    this.expression2 = expression2;
  }

  public interpreter(str: string): boolean {
    return this.expression1.interpreter(str) || this.expression2.interpreter(str);
  }
}

function buildInterpreterTree() {
  const terminal1: Expression = new TerminalExpression('A');
  const terminal2: Expression = new TerminalExpression('B');
  const terminal3: Expression = new TerminalExpression('C');
  const terminal4: Expression = new TerminalExpression('D');

  // B And C
  const alternation1: Expression = new AndExpression(terminal2, terminal3);
  // A Or (B C)
  const alternation2: Expression = new OrExpression(terminal1, alternation1);
  // D And (A Or (B C))
  return new AndExpression(terminal4, alternation2);
}

const define: Expression = buildInterpreterTree();
const context1: string = "D A";
const context2: string = "D B C";
console.log(define.interpreter(context1)); // true
console.log(define.interpreter(context2)); // true

参考文献:

图解23种设计模式(TypeScript版)

设计模式这样学也太简单了吧!


前端小白