Typescript玩转设计模式 之 建立型模式

做者简介 joey 蚂蚁金服·数据体验技术团队html

前言

咱们团队的工做是用单页面应用的方式实现web工具。涉及到数万到十数万行的前端代码的管理,并且项目周期长达数年。前端

怎么样很好地管理好这种量级的前端代码,在迭代的过程当中能保持代码的新鲜度,对咱们来讲是个挑战。java

一个运行良好的项目,除了要有好的架构外,还须要各个功能模块有良好的设计,学习设计模式,就是但愿能有技巧地设计新功能和重构已有代码。web

在网上看到不少说法,说学习设计模式做用不大,有些模式已通过时了,不学也能工做,学了反而容易过分设计。算法

我认为对事物的理解是“学习——领悟——突破”的过程。不懂的时候先学习,当学到的东西和实践经验有差别时结合思考能够领悟,等领悟到了其中的原理时,就能够不拘泥于学到的内容从而根据本身的场景灵活运用了。而过分设计显然仍是在学习和领悟之间而已。设计模式

设计模式也是这样,《设计模式》里列举的23种设计模式并非所有,模式的运用上每每也不是分得那么清楚,经常是多种模式混合使用。学习设计模式就像《倚天屠龙记》里张无忌学习太极拳同样,先学习招式,再打几遍,最终忘记这些招式。23种设计模式只是招式,咱们学习的目的是为了提升本身的设计水平,达到能结合场景信手拈来设计方案,不拘泥于招式的“大乘”境界。缓存

在学习设计模式的过程当中,我发现4人帮的原书demo代码是C++的,而网上设计模式文章的demo可能是java的。所以结合前端的js语言特性,整理了一遍各个模式的demo,方便有志于学习设计模式的同窗们理解,共同进步。bash

场景描述

假设咱们要实现一个迷宫,原始代码以下:数据结构

function createMazeDemo() {
  const maze = new Maze();
  
  const r1 = new Room(1);
  const r2 = new Room(2);
  const door = new Door(r1, r2);

  maze.addRoom(r1);
  maze.addRoom(r2);
  r1.setSide('east', new Wall());
  r1.setSide('west', door);
  
  return maze;
}

createMazeDemo();
复制代码

咱们已经实现了一个迷宫,这时候新的需求来了,迷宫里全部的东西都被施了魔法,但仍是要重用现有的布局(全部构件类,如Room、Wall、Door都要换成新的类)。架构

能够看到这样的硬编码方式不够灵活,那么如何改造createMaze方法以让他方便地用新类型的对象建立迷宫呢?

通用概念定义

  • 系统:整个程序生成的内容,如迷宫就是一个系统;
  • 产品:组成系统的对象,如迷宫的门,房间,墙分别是一种产品;

抽象工厂(Abstract factory)

定义

提供一个建立一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

结构

抽象工厂模式包含以下角色:

  • AbstractFactory:抽象工厂
  • ConcreteFactory:具体工厂
  • AbstractProduct:抽象产品
  • Product:具体产品

示例

// 迷宫的基类
class Maze {
  addRoom(room: Room): void {
  }
}
// 墙的基类
class Wall {
}
// 房间的基类
class Room {
  constructor(id: number) {
  }
  setSide(direction: string, content: Room|Wall): void {
  }
}
// 门的基类
class Door {
  constructor(roo1: Room, room2: Room) {
  }
}
// 迷宫工厂的基类,定义了生成迷宫各个构件的接口和默认实现,
// 子类能够复写接口的实现,返回不一样的具体类对象。
class MazeFactory {
  makeMaze(): Maze {
    return new Maze();
  }
  makeWall(): Wall {
    return new Wall();
  }
  makeRoom(roomId: number): Room {
    return new Room(roomId);
  }
  makeDoor(room1: Room, room2: Room): Door {
    return new Door(room1, room2);
  }
}

// 经过传入工厂对象,调用工厂的接口方法建立迷宫,
// 因为工厂的接口都是同样的,因此传入不一样的工厂对象,就能建立出不一样系列的具体产品
function createMazeDemo(factory: MazeFactory): Maze {
  const maze = factory.makeMaze();
  const r1 = factory.makeRoom(1);
  const r2 = factory.makeRoom(2);
  const door = factory.makeDoor(r1, r2);

  maze.addRoom(r1);
  maze.addRoom(r2);
  
  r1.setSide('east', factory.makeWall());
  r1.setSide('west', door);
  
  return maze;
}

// 标准系列工厂对象,工厂的每一个产品都是标准的
const standardSeries = new MazeFactory();
// 建立出标准的迷宫
createMazeDemo(standardSeries);

// 附了魔法的房间,继承自房间的基类
class MagicRoom extends Room {
  ...
}

// 附了魔法的门,继承自门的基类
class MagicDoor extends Door {
  ...
}

// 魔法系列的工厂,工厂的房间和门是被附了魔法的
class MagicMazeFactory extends MazeFactory {
  makeRoom(roomId: number): Room {
    return new MagicRoom(roomId);
  }
  makeDoor(room1: Room, room2: Room): Door {
    return new MagicDoor(room1, room2);
  }
  ...
}

// 魔法系列工厂对象,工厂建立出的门和房间是附了魔法的
const magicSeries = new MagicMazeFactory();
createMazeDemo(magicSeries);
复制代码

适用场景

  • 一系列相关的产品对象。若有魔法的房间和有魔法的门,都属于有魔法的迷宫构件,有必定相关性;
  • 系统要由多个产品系列的一个来配置。如当用户有多种迷宫样式风格能够选择,当选择黑色风格时,全部迷宫的构件都须要是黑色风格的构件;
  • 系统要独立于具体的产品类。如编写迷宫程序时,不须要关心用的是哪一个具体的房间类,只须要知道房间基类的接口就能够操做房间。各类迷宫工厂返回的房间类都是继承自房间基类,接口是一致的,即便普通房间换成了有魔法的房间,也不须要操做房间的代码,如迷宫的布局代码,关心改变。

优势

  • 分离了具体的类。如迷宫要改为有魔法的迷宫,迷宫的布局部分代码createMazeDemo不须要修改;
  • 易于交换产品系列。迷宫换成有魔法的迷宫时,只须要对createMazeDemo传入新的工厂对象便可;
  • 有利于产品的一致性。同一个系列的产品,都是相关性比较高的。如魔法迷宫的具体对象,都是带魔法的。

缺点

  • 难以支持新种类的产品。这是由于抽象工厂的接口肯定了能够被建立的产品集合,支持新种类的产品就须要扩展该工厂接口,这将涉及到抽象工厂类及其全部子类的改变。

相关模式

  • 抽象工厂模式一般用工厂方法实现,也能够用原型模式实现。
  • 一个具体的工厂一般是一个单例模式。

建造者(Builder)

定义

将一个复杂对象的构建与它的表示分离,使得一样的构建过程能够建立不一样的表示。

结构

建造者模式包含以下角色:

  • Builder:抽象建造者
  • ConcreteBuilder:具体建造者
  • Director:指挥者
  • Product:产品角色

示例

import { Maze, Wall, Room, Door } from './common';

// 迷宫建造者基类,定义了全部生成迷宫构件的接口,以及最终返回完整迷宫的接口
// 自身不建立迷宫,仅仅定义接口
class MazeBuilder {
  buildMaze(): void {}
  buildWall(roomId: number, direction: string): void {}
  buildRoom(roomId: number): void {}
  buildDoor(roomId1: number, roomId2: number): void {}
  getCommonWall(roomId1: number, roomId2: number): Wall { return new Wall(); };
  getMaze(): Maze|null { return null; }
}

// 建立迷宫的流程
// 相比最原始的代码,使用建造者模式只须要声明建造过程,而不须要知道建造过程当中用到的每一个构件的全部信息
// 好比,建造门的时候,只须要声明要建造一扇门,而不须要关心建造门的方法内部是如何将门与房间关联起来的
// 建造者模式只在迷宫被彻底建造完成时,才从建造者对象里取出整个迷宫,从而能很好地反映出完整的建造过程
function createMaze(builder: MazeBuilder) {
  builder.buildMaze();
  builder.buildRoom(1);
  builder.buildRoom(2);
  builder.buildDoor(1, 2);
  
  return builder.getMaze();
}

// 标准迷宫的建造者,继承自建造者基类
class StandardMazeBuilder extends MazeBuilder {
  currentMaze: Maze;
  constructor() {
    super();
    this.currentMaze = new Maze();
  }
  getMaze(): Maze|null {
    return this.currentMaze;
  }
  buildRoom(roomId: number): void {
    if (this.currentMaze) {
      const room = new Room(roomId);
      this.currentMaze.addRoom(room);

      room.setSide('north', new Wall());
      room.setSide('south', new Wall());
      room.setSide('east', new Wall());
      room.setSide('west', new Wall());
    }
  }
  buildDoor(roomId1: number, roomId2: number): void {
    const r1 = this.currentMaze.getRoom(roomId1);
    const r2 = this.currentMaze.getRoom(roomId2);
    const door = new Door(r1, r2);
    r1.setSide(this.getCommonWall(roomId1, roomId2), door);
    r2.setSide(this.getCommonWall(roomId2, roomId1), door);
  }
}

// 建造一个标准的迷宫
const standardBuilder = new StandardMazeBuilder();
createMaze(standardBuilder);

/**
 * 建造者也能够根本不建造具体的构件,而只是对建造过程进行计数。
 */
// 计数的数据结构声明
interface Count {
  rooms: number;
  doors: number;
}

// 不建立迷宫,只记数的建造者,也继承自建造者基类
class CountingMazeBuilder extends MazeBuilder {
  doors = 0;
  rooms = 0;
  buildRoom(): void {
    this.rooms += 1;
  }
  buildDoor(): void {
    this.doors += 1;
  }
  getCounts(): Count {
    return {
      rooms: this.rooms,
      doors: this.doors,
    };
  }
}

const countBuilder = new CountingMazeBuilder();
createMaze(countBuilder);
countBuilder.getCounts();
复制代码

适用场景

  • 当建立复杂对象的算法应该独立于该对象的组成部分以及他们的装配方式时。上例来看,建造迷宫的过程至关于算法,而每一个生成构件的方法包含了生成具体对象以及将具体对象装配到迷宫上的方式。同一个建造过程能够适配多种不一样的建造者,而同一个建造者能够支持多种不一样的建造过程,二者是相互独立的。
  • 当构造过程必须容许被构造的对象有不一样的表示时。建造过程相同,但须要支持多种不一样的装配方式,如上例中,相同的建造过程须要支持两种不一样的场景,一种场景须要建造出真实的迷宫,另外一种场景只须要统计建造的过程当中建了多少间房价和多少扇门。

优势

  • 能够改变一个产品的内部表示。接口能够隐藏产品的表示和内部结构,同时隐藏了产品的装配过程。由于产品是经过抽象接口构造的,你在改变该产品的内部表示时所要作的只是定义一个新的建造者。
  • 将构造代码和表示代码分开。建造者模式经过封装一个复杂对象的建立和表示方式提升了对象的模块性。客户不须要知道定义产品内部结构的类的全部信息。
  • 可对构造过程进行更精细的控制。建造者模式仅当该产品完成时才从建造者中取回它,所以建造者模式相比其余建立型模式能更好地反映产品的构造过程。

相关模式

  • 抽象工厂与建造者模式类似,由于它也能够建立复杂对象。主要的区别是建造者模式着重于一步步构造一个复杂对象。而抽象工厂着重于多个系列的产品对象。建造者在最后的一步返回产品,而对于抽象工厂来讲,产品是当即返回的。
  • 组合模式一般是用建造者模式生成的。

工厂方法(Factory Method)

定义

定义一个用于建立对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

结构

工厂方法模式包含以下角色:

  • Product:抽象产品
  • ConcreteProduct:具体产品
  • Factory:抽象工厂
  • ConcreteFactory:具体工厂

示例

子类决定实例化具体产品

import { Maze, Wall, Room, Door } from './common';

// 迷宫游戏类
class MazeGame {
  // 建立迷宫的主方法
  createMaze(): Maze {
    const maze = this.makeMaze();
    const r1 = this.makeRoom(1);
    const r2 = this.makeRoom(2);
    const door = this.makeDoor(r1, r2);

    maze.addRoom(r1);
    maze.addRoom(r2);

    r1.setSide('north', this.makeWall());
    r1.setSide('east', door);

    return maze;
  }
  // 如下是工厂方法,经过工厂方法建立构件,而不是直接在主方法中new出具体类
  // 工厂方法最重要的是定义出返回产品的接口,虽然这里提供了默认实现,但也能够只提供接口,让子类来实现
  makeMaze(): Maze { return new Maze(); }
  makeRoom(roomId: number): Room { return new Room(roomId); }
  makeWall(): Wall { return new Wall(); }
  makeDoor(room1: Room, room2: Room): Door { return new Door(room1, room2); }
}
// 建立普通迷宫游戏
const mazeGame = new MazeGame();
mazeGame.createMaze();

// 带炸弹的墙
class BombedWall extends Wall {
  ...
}
// 带炸弹的房间
class RoomWithABomb extends Room {
  ...
}
// 子类能够复写工厂方法,如下是带炸弹的迷宫游戏类
class BombedMazeGame extends MazeGame {
  // 复写建立墙的方法,返回一面带炸弹的墙
  makeWall(): Wall {
    return new BombedWall();
  }
  // 复写建立房间的方法,返回一个带炸弹的房间
  makeRoom(roomId: number): Room {
    return new RoomWithABomb(roomId);
  }
}
// 建立带炸弹的迷宫游戏
const bombedMazeGame = new BombedMazeGame();
bombedMazeGame.createMaze();
复制代码

参数化工厂方法

class Creator {
  createProduct(type: string): Product {
    if (type === 'normal') return new NormalProduct();
    if (type === 'black) return new BlackProduct(); return new DefaultProduct(); } } // 子类能够很容易地扩展或改变工厂方法返回的产品 class MyCreator extends Creator { createProduct(type: string): Product { // 改变产品 if (type === 'normal) return new MyNormalProduct();
    // 扩展新的产品
    if (type === 'white') return new WhiteProduct();
    // 注意这个操做的最后一件事是调用父类的`createProduct`,这是由于子类仅对某些type的处理上与父类不一样,对其余的type不感兴趣
    return Creator.prototype.createProduct.call(this, type);
  }
}
复制代码

适用场景

  • 当一个类不知道他所必须建立的对象的类的时候。
  • 当一个类但愿由他的子类来指定他所建立的对象的时候。
  • 当类将建立对象的职责委托给多个帮助子类的某一个,而且你但愿将哪个帮助子类是代理者这一信息局部化的时候。这就是参数化工厂方法的场景,将经过参数指定具体类的过程局部化在工厂方法中。

优势

  • 用工厂方法在一个类的内部建立对象一般比直接建立对象更灵活。相比于在createMaze方法中直接建立对象const r1 = new Room(1);,用工厂方法const r1 = this.makeRoom(1),能够在子类中复写makeRoom方法来实例化不一样的房间,能更灵活地应对需求变化。

相关模式

  • 抽象工厂常常用工厂方法来实现。
  • 工厂方法一般在模板方法模式中被调用。
  • 原型模式不须要建立子类,可是一般要求一个针对产品类的初始化操做。而工厂方法不须要这样的操做。

原型(Prototype)

定义

用原型实例指定建立对象的种类,而且经过复用这些原型建立新的对象。

示例

在其余语言里,原型模式是经过拷贝一个对象,而后修改新对象的属性,从而减小类的定义和实例化的开销。

但因为js自然支持prototype,所以原型的实现方式与其余类继承语言有些不一样,不须要经过对象提供clone方法来实现模型模式。

import { Maze, Wall, Room, Door } from './common';

interface Prototype {
  prototype?: any;
}

// 根据原型返回对象
function getNewInstance(prototype: Prototype, ...args: any[]): Wall|Maze|Room|Door {
  const proto = Object.create(prototype);
  const Kls = class {};
  Kls.prototype = proto;
  return new Kls(...args);
}

// 迷宫工厂,定义了生成构件的接口
class MazeFactory {
  makeWall(): Wall { return new Wall(); }
  makeDoor(r1: Room, r2: Room): Door { return new Door(r1, r2); }
  makeRoom(id: number): Room { return new Room(id); }
  makeMaze(): Maze { return new Maze(); }
}

// 原型迷宫工厂,根据初始化时传入的原型改变返回的迷宫构件
class MazePrototypeFactory extends MazeFactory {
  mazePrototype: Prototype;
  wallPrototype: Prototype;
  roomPrototype: Prototype;
  doorPrototype: Prototype;

  constructor(mazePrototype: Prototype, wallPrototype: Prototype, roomPrototype: Prototype, doorPrototype: Prototype) {
    super();
    this.mazePrototype = mazePrototype;
    this.wallPrototype = wallPrototype;
    this.roomPrototype = roomPrototype;
    this.doorPrototype = doorPrototype;
  }
  makeMaze() {
    return getNewInstance(this.mazePrototype);
  }
  makeRoom(id: number) {
    return getNewInstance(this.roomPrototype, id);
  }
  makeWall() {
    return getNewInstance(this.wallPrototype);
  }
  makeDoor(r1: Room, r2: Room): Door {
    const door = getNewInstance(this.doorPrototype, r1, r2);
    return door;
  }
}

// 建立迷宫的过程
function createMaze(factory: MazeFactory): Maze {
  const maze = factory.makeMaze();
  const r1 = factory.makeRoom(1);
  const r2 = factory.makeRoom(2);
  const door = factory.makeDoor(r1, r2);

  maze.addRoom(r1);
  maze.addRoom(r2);
  
  r1.setSide('east', factory.makeWall());
  r1.setSide('west', door);
  
  return maze;
}

// 各个迷宫构件的原型
const mazePrototype = {...};
const wallPrototype = {...};
const roomPrototype = {...};
const doorPrototype = {...};

// 生成简单的迷宫
const simpleMazeFactory = new MazePrototypeFactory(mazePrototype, wallPrototype, roomPrototype, doorPrototype);
createMaze(simpleMazeFactory);

// 带有炸弹的迷宫构件的原型
const bombedWallPrototype = {...};
const roomWithABombPrototype = {...};
// 生成带有炸弹的迷宫
const bombedMazeFactory = new MazePrototypeFactory(mazePrototype, bombedWallPrototype, roomWithABombPrototype, doorPrototype);
createMaze(bombedMazeFactory);
复制代码

适用场景

  • 要实例化的类是在运行时刻指定时。如经过动态加载的方式拿到原型后,在程序运行的过程当中根据原型直接建立出实例,而不是经过预先定义好的class new出对象。或者类须要根据运行环境动态改变,能够经过修改原型来产生不一样的对象。

优势

  • 运行时增长产品。由于能够在运行时动态根据原型生成新的种类的对象。
  • 减小子类的构造,减小在系统中用到的类的数量。
  • 用类动态配置应用。在运行时动态加载类。

相关模式

  • 原型模式和抽象工厂模式在某种方面是互相竞争的。可是他们也能够一块儿使用。抽象工厂能够存储一个原型的集合,而且返回产品对象。
  • 大量使用组合模式和装饰模式的设计一般也能够从原型模式中获益。

单例(Singleton)

定义

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

结构

单例模式包含以下角色:

  • Singleton:单例

示例

简单的单例

class MazeFactory {
  // 将constructor设为私有,防止经过new该类产生多个对象,破坏单例
  private constructor() {}
  static instance: MazeFactory;
  // 若是已经有了对象,则返回缓存的对象,否则就建立一个对象并缓存,保证系统内最多只有一个该类的对象
  static getInstance(): MazeFactory {
    if (!MazeFactory.instance) {
      MazeFactory.instance = new MazeFactory();
    }
    return MazeFactory.instance;
  }
}
复制代码

根据参数选择要实例化的迷宫工厂

class BombedMazeFactory extends MazeFactory {
  ...
}

class AdvancedMazeFactory {
  // 将constructor设为私有,防止经过new该类产生多个对象,破坏单例
  private constructor() {}
  static instance: MazeFactory;
  static getInstance(type: string): MazeFactory {
    if (!AdvancedMazeFactory.instance) {
      if (type === 'bombed') {
        AdvancedMazeFactory.instance = new BombedMazeFactory();
      } else {
        AdvancedMazeFactory.instance = new MazeFactory();
      }
    }
    return AdvancedMazeFactory.instance;
  }
}
复制代码

适用场景

  • 当类只能有一个实例,并且使用者能够从一个众所周知的访问点访问它时;
  • 当这个惟一的实例应该是经过子类化可扩展的,如上例中根据参数扩展,而且使用者应该无需更改代码就能使用一个扩展的实例时;

优势

  • 对惟一实例能够进行有效的受控访问。
  • 防止存储惟一实例的全局变量污染命名空间。
  • 能够在实例化方法中改变具体使用的实例。
  • 容许可变数目的实例。这个模式使你易于改变你的想法,并容许单例的类同时存在多个实例。由于实例化的入口在一个地方,能够方便地控制容许同时存在的实例数量。

相关模式

  • 不少模式能够用单例实现,如抽象工厂,建造者,和原型。

参考文档

本文只介绍了建立型模式,对后续模式感兴趣的同窗能够关注专栏或者发送简历至'chaofeng.lcf####alibaba-inc.com'.replace('####', '@'),欢迎有志之士加入~

原文地址:https://juejin.im/post/59fa88ac5188255a6a0d5f31

相关文章
相关标签/搜索