Typescript玩轉設計模式 之 對象行爲型模式(上)

做者簡介 joey 螞蟻金服·數據體驗技術團隊html

繼前面幾篇設計模式文章以後,這篇介紹5個對象行爲型設計模式。java

Chain of Responsibility(職責鏈)

意圖

使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關係。將這些對象連成一條鏈,並沿着這條鏈傳遞該請求,直到有一個對象處理他爲止。git

結構

職責鏈模式包含以下角色:github

  • Handler(抽象處理者):它定義了一個處理請求的接口,通常設計爲抽象類,因爲不一樣的具體處理者處理請求的方式不一樣,所以在其中定義了抽象請求處理方法。由於每個處理者的下家仍是一個處理者,所以在抽象處理者中定義了一個抽象處理者類型的對象(如結構圖中的successor),做爲其對下家的引用。經過該引用,處理者能夠連成一條鏈。
  • ConcreteHandler(具體處理者):它是抽象處理者的子類,能夠處理用戶請求,在具體處理者類中實現了抽象處理者中定義的抽象請求處理方法,在處理請求以前須要進行判斷,看是否有相應的處理權限,若是能夠處理請求就處理它,不然將請求轉發給後繼者;在具體處理者中能夠訪問鏈中下一個對象,以便請求的轉發。

示例

interface RequestData {
    name: string,
    increaseNum: number,
  }

  /**
   * 抽象處理者
   */
  abstract class Handler {
    protected next: Handler;
    setNext(next: Handler) {
      this.next = next;
    }
    abstract processRequest(request: RequestData): void;
  }

  class IdentityValidator extends Handler {
    processRequest(request: RequestData) {
      if (request.name === 'yuanfeng') {
        console.log(`${request.name} 是本公司的員工`);
        this.next.processRequest(request);
      } else {
        console.log('不是本公司員工');
      }
    }
  }

  class Manager extends Handler {
    processRequest(request: RequestData) {
      if (request.increaseNum < 300) {
        console.log('低於300的漲薪,經理直接批准了');
      } else {
        console.log(`${request.name}的漲薪要求超過了經理的權限,須要更高級別審批`);
        this.next.processRequest(request);
      }
    }
  }

  class Boss extends Handler {
    processRequest(request: RequestData) {
      console.log('hehe,想漲薪,你能夠走了');
    }
  }

  function chainOfResponsibilityDemo() {
    const identityValidator = new IdentityValidator();
    const manager = new Manager();
    const boss = new Boss();
    // 構建職責鏈
    identityValidator.setNext(manager);
    manager.setNext(boss);

    const request: RequestData = {
      name: 'yuanfeng',
      increaseNum: 500,
    };
    identityValidator.processRequest(request);
  }

  chainOfResponsibilityDemo();
複製代碼

適用場景

  • 有多個對象能夠處理一個請求,哪一個對象處理該請求運行時自動肯定,客戶端只須要把請求提交到鏈上便可;
  • 想在不明確指定接收者的狀況下,向多個對象中的一個提交一個請求;
  • 可處理一個請求的對象集合應被動態指定;

優勢

  • 下降耦合度。鏈中的對象不需知道鏈的結構;
  • 加強了職責鏈組織的靈活性。能夠在運行時動態改變職責鏈;

缺點

  • 不保證被接受。一個請求可能得不處處理;
  • 若是建鏈不當,可能會形成循環調用,將致使系統陷入死循環;

相關模式

  • 職責鏈經常與Composite(組合模式)一塊兒使用。一個對象的父對象能夠做爲他的後繼者。

Command(命令)

意圖

將一個請求封裝爲一個對象,從而使你可用不一樣的請求對客戶進行參數化;對請求排隊或記錄請求日誌,以及支持可撤銷的操做。編程

結構

命名模式包含如下角色:設計模式

  • Command(抽象命令類):抽象命令類通常是一個抽象類或接口,在其中聲明瞭用於執行請求的execute()等方法,經過這些方法能夠調用請求接收者的相關操做。
  • ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了在抽象命令類中聲明的方法,它對應具體的接收者對象,將接收者對象的動做綁定其中。在實現execute()方法時,將調用接收者對象的相關操做(Action)。
  • Invoker(調用者):調用者即請求發送者,它經過命令對象來執行請求。一個調用者並不須要在設計時肯定其接收者,所以它只與抽象命令類之間存在關聯關係。在程序運行時能夠將一個具體命令對象注入其中,再調用具體命令對象的execute()方法,從而實現間接調用請求接收者的相關操做。
  • Receiver(接收者):接收者執行與請求相關的操做,它具體實現對請求的業務處理。

示例

簡單命令

// 點菜場景下,客戶點餐後徹底不須要知道作菜的廚師是誰,記載着客戶點菜信息的訂單就是一個命令。

  // 命令的基類,只包含了一個執行方法
  class Command {
    execute(arg?): void {}
  }

  // 廚師類,每一個廚師都會作麪包和肉
  class Cook {
    private name: string;
    constructor(name: string) {
      this.name = name;
    }
    makeBread() {
      console.log(`廚師 ${this.name} 在作麪包`);
    }
    makeMeal() {
      console.log(`廚師 ${this.name} 在作肉`);
    }
  }

  // 簡單命令只須要包含接收者和執行接口
  class SimpleCommand extends Command {
    // 接收者,在點菜系統裏是廚師
    receiver: Cook;
  }

  // 作麪包的命令類
  class BreadCommand extends SimpleCommand {
    constructor(cook: Cook) {
      super();
      this.receiver = cook;
    }
    execute() {
      this.receiver.makeBread();
    }
  }

  // 作肉的命令類
  class MealCommand extends SimpleCommand {
    constructor(cook: Cook) {
      super();
      this.receiver = cook;
    }
    execute() {
      this.receiver.makeMeal();
    }
  }

  // 系統啓動時,將命令註冊到菜單上,生成可被處處使用的命令對象
  function simpleCommandDemo(): void {
    const cook1 = new Cook('廚師1');
    const cook2 = new Cook('廚師2');

    // 生成菜單,上架銷售,顧客能夠選擇點肉或點麪包
    const breadCommand: Command = new BreadCommand(cook1);
    const mealCommand: Command = new MealCommand(cook2);

    // 客戶點菜時,徹底不須要知道是哪一個廚師作的,只須要從菜單上點想要的菜,即下命令便可
    // 此時已經作到了命令的觸發者與接收者的分離
    // 命令對象能夠在整個系統中處處傳遞,如通過多個服務員,而不會丟失接受者的信息
    breadCommand.execute();
    mealCommand.execute();
  }
複製代碼

可撤銷命令

相比簡單命令,除了在命令對象中保存了接收者,還須要存儲額外的狀態信息,如接收者上次執行操做的參數數組

class AdvancedCommand extends Command {
  // 接收者
  ball: Ball;
  // 額外狀態信息,移動的距離
  pos: number;
  // 執行命令時候,向左移動,同時記錄下移動的距離
  execute(pos: number) {
    this.pos = pos;
    this.ball.moveToLeft(pos);
  }
  // 撤銷時執行反向操做
  unExecute() {
    this.ball.moveToRight(this.pos);
  }
}
複製代碼

宏命令

同時容許多個命令,這裏不須要顯式的接收者,由於每一個命令都已經定義了各自的接收者bash

class MacroCommand extends Command {
  // 保存命令列表
  cmdSet: Set<Command> = [];
  add(cmd: Command): void {
    this.cmdSet.add(cmd);
  }
  remove(cmd: Command): void {
    this.cmdSet.delete(cmd);
  }
  execute(): void {
    this.cmdSet.forEach((cmd: Command) => {
      cmd.execute();
    });
  }
}
複製代碼

適用場景

  • 菜單場景。抽象出待執行的動做以參數化某對象。你可用過程語言中的「回調」函數表達這種參數化機制。所謂回調函數是指函數先在某處註冊,而它將在稍後某個須要的時候被調用。Commond模式是回調機制的一個面向對象的替代品。
  • 在不一樣的時刻指定、排列和執行請求。一個Command對象能夠有一個與初始請求無關的生存期。若是一個請求的接收者可用一種與地址空間無關的方式表達,那麼就可將負責該請求的命令對象傳送給另外一個不一樣的進程並在那兒實現該請求。
  • 支持取消操做。Command的Excute操做可在實施操做前將狀態存儲起來,在取消操做時這個狀態用來消除該操做的影響。Command接口必須添加一個Unexecute操做,該操做取消上一次Execute調用的效果。執行的命令被存儲在一個歷史列表中。可經過向後和向前遍歷這一列表並分別調用Unexecute和Execute來實現重數不限的「取消」和「重作「。

優勢

  • 將調用操做的對象與知道如何實現該操做的對象解耦;
  • 能夠將多個命令裝配成一個宏命令;
  • 增長新的命令很容易,由於無需改變已有的類;
  • 爲請求的撤銷和恢復操做提供了一種設計和實現方案;

缺點

  • 可能會致使系統裏有過多的具體命令類。由於針對每個對請求接收者的調用操做都須要設計一個具體命令類,所以在系統中可能須要提供大量的具體命令類,這將影響命令模式的使用。

相關模式

  • 組合模式可被用來實現宏命令
  • 備忘錄模式可被用來保持某個狀態,命令用這一狀態來作撤銷

Iterator(迭代器)

意圖

提供一種方法順序訪問一個聚合對象中各個元素,而又不需暴露該對象的內部表示。編程語言

結構

迭代器模式包含如下角色:ide

  • Iterator(抽象迭代器):它定義了訪問和遍歷元素的接口,聲明瞭用於遍歷數據元素的方法,例如:用於獲取第一個元素的first()方法,用於訪問下一個元素的next()方法,用於判斷是否還有下一個元素的hasNext()方法,用於獲取當前元素的currentItem()方法等,在具體迭代器中將實現這些方法。
  • ConcreteIterator(具體迭代器):它實現了抽象迭代器接口,完成對聚合對象的遍歷,同時在具體迭代器中經過遊標來記錄在聚合對象中所處的當前位置,在具體實現時,遊標一般是一個表示位置的非負整數。
  • Aggregate(抽象聚合類):它用於存儲和管理元素對象,聲明一個createIterator()方法用於建立一個迭代器對象,充當抽象迭代器工廠角色。
  • ConcreteAggregate(具體聚合類):它實現了在抽象聚合類中聲明的createIterator()方法,該方法返回一個與該具體聚合類對應的具體迭代器ConcreteIterator實例。

示例

相對於迭代器模式的經典結構,簡化了實現,去除了抽象聚合類和具體聚合類的設計,同時簡化了迭代器接口。

// 迭代器接口
interface Iterator {
  next(): any;
  first(): any;
  isDone(): boolean;
}

// 順序挨個遍歷數組的迭代器
class ListIterator implements Iterator {
  protected list: Array<any> = [];
  protected index: number = 0;
  constructor(list) {
    this.list = list;
  }
  first() {
    if (this.list.length) {
      return this.list[0];
    }
    return null;
  }
  next(): any {
    if (this.index < this.list.length) {
      this.index += 1;
      return this.list[this.index];
    }
    return null;
  }
  isDone(): boolean {
    return this.index >= this.list.length;
  }
}

// 跳着遍歷數組的迭代器
// 因爲跳着遍歷和逐個遍歷,區別只在於next方法,所以經過繼承簡單實現
class SkipIterator extends ListIterator {
  next(): any {
    if (this.index < this.list.length) {
      const nextIndex = this.index + 2;
      if (nextIndex < this.list.length) {
        this.index = nextIndex;
        return this.list[nextIndex];
      }
    }
    return null;
  }
}

// 對同一個序列,調用不一樣的迭代器就能實現不一樣的遍歷方式,而不須要將迭代方法寫死在序列中
// 經過迭代器的方式,將序列與遍歷方法分離
function iteratorDemo(): void {
  const list = [1,2,3,4,5,6];

  // 挨個遍歷
  const listIterator: Iterator = new ListIterator(list);
  while(!listIterator.isDone()) {
    const item: number = listIterator.next();
    console.log(item);
  }

  // 跳着遍歷
  const skipIterator: Iterator = new SkipIterator(list);
  while(!listIterator.isDone()) {
    const item: number = skipIterator.next();
    console.log(item);
  }
}

// 內部迭代器,即在聚合內部定義的迭代器,外部調用不須要關心迭代器的具體實現,缺點是功能被固定,不易擴展
class SkipList {
  list = [];
  constructor(list: Array<any>) {
    this.list = list;
  }
  // 內部定義了遍歷的規則
  // 這裏實現爲間隔遍歷
  loop(callback) {
    if (this.list.length) {
      let index = 0;
      const nextIndex = index + 2;
      if (nextIndex < this.list.length) {
        callback(this.list[nextIndex]);
        index = nextIndex;
      }
    }
  }
}

function innerIteratorDemo(): void {
  const list = [1,2,3,4,5,6];
  const skipList = new SkipList(list);
  // 按照聚合的內部迭代器定義的規則迭代
  skipList.loop(item => {
    console.log(item);
  });
}
複製代碼

適用場景

  • 訪問一個聚合對象的內容而無需暴露它的內部結構;
  • 支持對聚合對象的多種遍歷方式;
  • 爲遍歷不一樣的聚合結構提供一個統一的接口;

優勢

  • 它支持以不一樣的方式遍歷一個聚合對象,在同一個聚合對象上能夠定義多種遍歷方式;
  • 迭代器簡化了聚合類。因爲引入了迭代器,在原有的聚合對象中不須要再自行提供數據遍歷等方法,這樣能夠簡化聚合類的設計;
  • 在迭代器模式中,因爲引入了抽象層,增長新的聚合類和迭代器類都很方便,無須修改原有代碼,知足「開閉原則」的要求;

缺點

  • 因爲迭代器模式將存儲數據和遍歷數據的職責分離,增長新的聚合類須要對應增長新的迭代器類,類的個數成對增長,這在必定程度上增長了系統的複雜性;
  • 抽象迭代器的設計難度較大,須要充分考慮到系統未來的擴展。在自定義迭代器時,建立一個考慮全面的抽象迭代器並非件很容易的事情。

相關模式

  • 組合模式:迭代器常被應用到像組合模式這樣的遞歸結構上;
  • 工廠方法:多態迭代器靠工廠方法來實例化適當的迭代器子類;
  • 備忘錄:常與迭代器模式一塊兒使用。迭代器可以使用一個備忘錄來捕獲一個迭代的狀態。迭代器在其內部存儲備忘錄;

Mediator(中介者)

意圖

用一箇中介對象來封裝一系列的對象交互,中介者使各對象不須要顯式地相互引用,從而使其耦合鬆散,並且能夠獨立地改變它們之間的交互。

結構

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

  • Mediator(抽象中介者):它定義一個接口,該接口用於與各同事對象之間進行通訊。
  • ConcreteMediator(具體中介者):它是抽象中介者的子類,經過協調各個同事對象來實現協做行爲,它維持了對各個同事對象的引用。
  • Colleague(抽象同事類):它定義各個同事類公有的方法,並聲明瞭一些抽象方法來供子類實現,同時它維持了一個對抽象中介者類的引用,其子類能夠經過該引用來與中介者通訊。
  • ConcreteColleague(具體同事類):它是抽象同事類的子類;每個同事對象在須要和其餘同事對象通訊時,先與中介者通訊,經過中介者來間接完成與其餘同事類的通訊;在具體同事類中實現了在抽象同事類中聲明的抽象方法。

示例

租房的案例,租客和房主經過中介者聯繫,二者並不直接聯繫

// 抽象中介者
  abstract class Mediator {
    abstract contact(message: string, person: Human): void
  }

  // 抽象同事類
  abstract class Human {
    name: string
    mediator: Mediator
    constructor(name: string, mediator: Mediator) {
      this.name = name;
      this.mediator = mediator;
    }
  }

  // 2個具體的同事類
  // 房主類
  class HouseOwner extends Human {
    contact(message: string) {
      console.log(`房主 ${this.name} 發送消息 ${message}`);
      this.mediator.contact(message, this);
    }
    getMessage(message: string) {
      console.log(`房主 ${this.name} 收到消息 ${message}`);
    }
  }

  // 租客類
  class Tenant extends Human {
    contact(message: string) {
      console.log(`租客 ${this.name} 發送消息 ${message}`);
      this.mediator.contact(message, this);
    }
    getMessage(message: string) {
      console.log(`租客 ${this.name} 收到消息 ${message}`);
    }
  }

  // 具體中介者
  class ConcreteMediator extends Mediator {
    private tenant: Tenant;
    private houseOwner: HouseOwner;
    setTenant(tenant: Tenant) {
      this.tenant = tenant;
    }
    setHouseOwner(houseOwner: HouseOwner) {
      this.houseOwner = houseOwner;
    }
    // 由中介者來設置同事對象之間的聯繫關係
    contact(message: string, person: Human) {
      console.log('中介傳遞消息');
      if (person === this.houseOwner) {
        this.tenant.getMessage(message);
      } else {
        this.houseOwner.getMessage(message);
      }
    }
  }

  function mediatorDemo() {
    const mediator = new ConcreteMediator();
    const houseOwner = new HouseOwner('財大氣粗的房叔', mediator);
    const tenant = new Tenant('遠峯', mediator);
    // 向中介者註冊成員
    mediator.setHouseOwner(houseOwner);
    mediator.setTenant(tenant);
    // 中介的成員只須要發送信息,而不須要關心具體接受者,聯繫關係都維護在了中介者中
    tenant.contact('我想租房');
    houseOwner.contact('我有房,你要租嗎');
  }
複製代碼

適用場景

  • 一組對象以定義良好可是複雜的方式進行通訊,產生的相互依賴關係結構混亂且難以理解;
  • 一個對象引用其餘不少對象而且直接與這些對象通訊,致使難以複用該對象;
  • 想經過一箇中間類來封裝多個類中的行爲,而又不想生成太多的子類;

優勢

  • 簡化了對象之間的關係,將系統的各個對象之間的相互關係進行封裝,將各個同事類解耦,使系統成爲鬆耦合系統;
  • 使控制集中化。將交互的複雜性變爲中介者的複雜性;
  • 減小了子類的生成;
  • 能夠減小各同事類的設計與實現;

缺點

  • 因爲中介者對象封裝了系統中對象之間的相互關係,致使其變得很是複雜,可能難以維護。

相關模式

  • 外觀模式與中介者的不一樣之處在於它是對一個對象子系統進行抽象,從而提供了一個更爲方便的接口。它的協議是單向的,即外觀對象對這個子系統類提出請求,但反之則不行。相反,中介者提供了各同事對象不支持或不能支持的協做行爲,並且協議是多向的。
  • 同事對象可以使用觀察者模式與中介者對象通訊。

Memento(備忘錄)

意圖

在不破壞封裝性的前提下,捕獲一個對象的內部狀態,並在該對象以外保存這個狀態。這樣之後就可將該對象恢復到原先保存的狀態。

結構

備忘錄模式包含如下角色:

  • Originator(原發器):它是一個普通類,能夠建立一個備忘錄,並存儲它的當前內部狀態,也可使用備忘錄來恢復其內部狀態,通常將須要保存內部狀態的類設計爲原發器。
  • Memento(備忘錄):存儲原發器的內部狀態,根據原發器來決定保存哪些內部狀態。備忘錄的設計通常能夠參考原發器的設計,根據實際須要肯定備忘錄類中的屬性。須要注意的是,除了原發器自己與負責人類以外,備忘錄對象不能直接供其餘類使用,原發器的設計在不一樣的編程語言中實現機制會有所不一樣。
  • Caretaker(負責人):負責人又稱爲管理者,它負責保存備忘錄,可是不能對備忘錄的內容進行操做或檢查。在負責人類中能夠存儲一個或多個備忘錄對象,它只負責存儲對象,而不能修改對象,也無須知道對象的實現細節。

示例

案例:一個角色在畫布中移動

// 備忘錄類
class Memento {
  private x: number;
  private y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
  getX(): number {
    return this.x;
  }
  getY(): number {
    return this.y;
  }
}

// 原發器類
class Role {
  private x: number;
  private y: number;
  constructor(name: string, x: number, y: number) {
    this.x = x;
    this.y = y;
  }
  // 移動到新的位置
  moveTo(x: number, y: number): Memento {
    this.x = x;
    this.y = y;
    return this.save();
  }
  save(): Memento {
    return new Memento(this.x, this.y);
  }
  // 根據備忘錄回退到某一個位置
  goBack(memento: Memento) {
    this.x = memento.getX();
    this.y = memento.getY();
  }
}

// 負責人,管理全部備忘錄
class HistoryRecords {
  private records = [];
  // 添加備忘錄
  add(record: Memento): void {
    this.records.push(record);
  }
  // 返回備忘錄
  get(index: number): Memento {
    if (this.records[index]) {
      return this.records[index];
    }
    return null;
  }
  // 清除指定位置後面的備忘錄
  cleanRecordsAfter(index: number): void {
    this.records.slice(0, index + 1);
  }
}

// 客戶代碼
function mementoDemo() {
  const role = new Role('卡通小人', 0, 0);
  const records = new HistoryRecords();
  // 記錄初始位置
  records.add(role.save());
  // 移動時添加備忘錄
  role.moveTo(10, 10);
  records.add(role.save());
  role.moveTo(20, 30);
  records.add(role.save());
  // 回退到初始位置
  const GO_BACK_STEP = 0;
  const firstMemento = records.get(GO_BACK_STEP);
  role.goBack(firstMemento);
  // 清除後面的記錄
  records.cleanRecordsAfter(GO_BACK_STEP);
}
複製代碼

適用場景

  • 必須保存一個對象在某一個時刻的(部分)狀態,這樣之後須要時它才能恢復到先前的狀態;
  • 若是一個對象用接口來讓其餘對象直接獲得內部狀態,將會暴露對象的實現細節並破壞對象的封裝性;

優勢

  • 保持封裝邊界。使用備忘錄能夠避免暴露一些只應由原發器管理卻又必須存儲在原發器以外的信息。
  • 簡化原發器。相對於把全部狀態管理重任交給原發器,讓客戶管理他們請求的狀態將會簡化原發器,而且使得客戶工做結束時無需通知原發器。

缺點

  • 使用備忘錄代價可能很高。若是原發器在生成備忘錄時必須拷貝並存儲大量的信息,或者客戶很是頻繁地建立備忘錄和恢復原發器狀態,可能致使很大的開銷。除非封裝和恢復狀態的開銷不打,不然該模式可能並不適合。
  • 維護備忘錄存在潛在代價。管理器負責刪除它所維護的備忘錄,然而管理器在運行過程當中不肯定會存入多少備忘錄,所以可能原本很小的管理器,會產生大量的存儲開銷。

相關模式

  • 命令模式:命令可以使用備忘錄來爲可撤銷的操做維護狀態;
  • 迭代器模式:備忘錄可用於迭代;

參考文檔

本文介紹了5種對象行爲型模式,對後續模式感興趣的同窗能夠關注專欄或者發送簡歷至'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~

原文地址:github.com/ProtoTeam/b…

相關文章
相關標籤/搜索