Typescript玩轉設計模式 之 物件行為型模式(上)

螞蟻金服資料體驗技術發表於2018-01-28

作者簡介 joey 螞蟻金服·資料體驗技術團隊

繼前面幾篇設計模式文章之後,這篇介紹5個物件行為型設計模式。

Chain of Responsibility(職責鏈)

意圖

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

結構

職責鏈模式包含如下角色:

  • Handler(抽象處理者):它定義了一個處理請求的介面,一般設計為抽象類,由於不同的具體處理者處理請求的方式不同,因此在其中定義了抽象請求處理方法。因為每一個處理者的下家還是一個處理者,因此在抽象處理者中定義了一個抽象處理者型別的物件(如結構圖中的successor),作為其對下家的引用。通過該引用,處理者可以連成一條鏈。
  • ConcreteHandler(具體處理者):它是抽象處理者的子類,可以處理使用者請求,在具體處理者類中實現了抽象處理者中定義的抽象請求處理方法,在處理請求之前需要進行判斷,看是否有相應的處理許可權,如果可以處理請求就處理它,否則將請求轉發給後繼者;在具體處理者中可以訪問鏈中下一個物件,以便請求的轉發。

Typescript玩轉設計模式 之 物件行為型模式(上)

示例

  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(接收者):接收者執行與請求相關的操作,它具體實現對請求的業務處理。

Typescript玩轉設計模式 之 物件行為型模式(上)

示例

簡單命令

  // 點菜場景下,客戶點餐後完全不需要知道做菜的廚師是誰,記載著客戶點菜資訊的訂單就是一個命令。

  // 命令的基類,只包含了一個執行方法
  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);
  }
}
複製程式碼

巨集命令

同時允許多個命令,這裡不需要顯式的接收者,因為每個命令都已經定義了各自的接收者

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(迭代器)

意圖

提供一種方法順序訪問一個聚合物件中各個元素,而又不需暴露該物件的內部表示。

結構

迭代器模式包含以下角色:

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

Typescript玩轉設計模式 之 物件行為型模式(上)

示例

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

// 迭代器介面
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(具體同事類):它是抽象同事類的子類;每一個同事物件在需要和其他同事物件通訊時,先與中介者通訊,通過中介者來間接完成與其他同事類的通訊;在具體同事類中實現了在抽象同事類中宣告的抽象方法。

Typescript玩轉設計模式 之 物件行為型模式(上)

示例

租房的案例,租客和房主通過中介者聯絡,兩者並不直接聯絡

  // 抽象中介者
  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(負責人):負責人又稱為管理者,它負責儲存備忘錄,但是不能對備忘錄的內容進行操作或檢查。在負責人類中可以儲存一個或多個備忘錄物件,它只負責儲存物件,而不能修改物件,也無須知道物件的實現細節。

Typescript玩轉設計模式 之 物件行為型模式(上)

示例

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

// 備忘錄類
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…

相關文章