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

相關文章
相關標籤/搜索