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

螞蟻金服資料體驗技術發表於2018-02-04

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

本文是typescript設計模式系列文章的最後一篇,介紹了最後5個物件行為型的設計模式~

  • 觀察者模式
  • 狀態模式
  • 策略模式
  • 模板模式
  • 訪問者模式

Observer(觀察者)

意圖

定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。

結構

觀察者模式包含以下角色:

  • Subject(目標):目標又稱為主題,它是指被觀察的物件。在目標中定義了一個觀察者集合,一個觀察目標可以接受任意數量的觀察者來觀察,它提供一系列方法來增加和刪除觀察者物件,同時它定義了通知方法notify()。目標類可以是介面,也可以是抽象類或具體類。
  • ConcreteSubject(具體目標):具體目標是目標類的子類,通常它包含有經常發生改變的資料,當它的狀態發生改變時,向它的各個觀察者發出通知;同時它還實現了在目標類中定義的抽象業務邏輯方法(如果有的話)。如果無須擴充套件目標類,則具體目標類可以省略。
  • Observer(觀察者):觀察者將對觀察目標的改變做出反應,觀察者一般定義為介面,該介面宣告瞭更新資料的方法update(),因此又稱為抽象觀察者。
  • ConcreteObserver(具體觀察者):在具體觀察者中維護一個指向具體目標物件的引用,它儲存具體觀察者的有關狀態,這些狀態需要和具體目標的狀態保持一致;它實現了在抽象觀察者Observer中定義的update()方法。通常在實現時,可以呼叫具體目標類的attach()方法將自己新增到目標類的集合中或通過detach()方法將自己從目標類的集合中刪除。

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

示例

推模型

目標向觀察者傳送關於改變的“詳細資訊”,而不管它們需要與否。由目標維護觀察者。

  // 場景:顧客點菜後,服務員記下顧客的資訊,菜做好後廣播通知顧客領取

  // 觀察者基類
  class Observer {
    take(msg: string): void {}
  }

  // 目標基類
  class Subject {
    set: Set<Observer> = new Set();
    // 註冊回撥
    add(observer: Observer): void {
      this.set.add(observer);
    }
    // 登出回撥
    remove(observer: Observer): void {
      this.set.delete(observer);
    }
    // 觸發所有已註冊的回撥
    notify(msg: string): void {
      this.set.forEach(observer => {
        observer.take(msg);
      });
    }
  }

  // 具體目標,服務員類
  class Waiter extends Subject {
    // 菜做完後通知所有註冊了的顧客
    ready(): void {
      this.notify('ready');
    }
  }

  // 具體觀察者,顧客類
  class Client extends Observer {
    name: string;
    // 初始化時將自身註冊到目標,以便接收通知
    constructor(name: string, waiter: Waiter) {
      super();
      this.name = name;
      waiter.add(this);
    }
    take(msg: string) {
      console.log(`顧客 ${this.name} 收到了訊息顯示狀態是<${msg}>, 到吧檯領取了菜`);
    }
  }

  function observerPushDemo() {
    const waiter = new Waiter();
    // 顧客點菜後,等待服務員通知
    const bob = new Client('Bob', waiter);
    const mick = new Client('Mick', waiter);
    // 菜準備好後,服務員廣播通知顧客可以到吧檯領取了
    waiter.ready();
  }
複製程式碼

拉模型

目標除了“最小通知”外什麼也不送出,而在此之後由觀察者顯式地向目標詢問細節。觀察者裡維護了目標物件。

  // 場景:顧客點菜後,收到通知從服務員處詢問詳細資訊

  // 觀察者基類
  class Observer {
    take(subject: Subject): void {}
  }

  // 目標基類
  class Subject {
    set: Set<Observer> = new Set();
    // 註冊回撥
    add(observer: Observer): void {
      this.set.add(observer);
    }
    // 登出回撥
    remove(observer: Observer): void {
      this.set.delete(observer);
    }
    // 觸發所有已註冊的回撥
    notify(): void {
      this.set.forEach(observer => {
        observer.take(this);
      });
    }
  }

  // 具體目標,服務員類
  class Waiter extends Subject {
    status = 'doing';
    // 與推模式的區別是,只傳送通知,不傳送詳細資料
    ready(): void {
      this.status = 'ready';
      this.notify();
    }
    // 提供訪問詳細資料介面,讓觀察者訪問詳細資料
    getStatus(): string {
      return this.status;
    }
  }

  // 具體觀察者,顧客類
  class Client extends Observer {
    name: string;
    // 初始化時將自身註冊到目標,以便接收通知
    constructor(name: string, waiter: Waiter) {
      super();
      this.name = name;
      waiter.add(this);
    }
    // 與推模式的區別是,收到通知後,沒有資料傳入,需要從目標裡讀取
    take(waiter: Waiter) {
      const msg = waiter.getStatus();
      console.log(`顧客 ${this.name} 收到通知,詢問服務員後發現狀態是 <${msg}> 後領取了菜`);
    }
  }

  function observerPushDemo() {
    const waiter = new Waiter();
    // 顧客點菜
    const bob = new Client('Bob', waiter);
    const mick = new Client('Mick', waiter);
    // 菜準備完後,服務員通知了下所有顧客狀態改變了,但沒有傳送內容出去,需要顧客再詢問一下服務員才知道最新狀態
    waiter.ready();
  }
複製程式碼

適用場景

  • 當一個抽象模型有兩個方面,其中一個方面依賴於另一方面。將這兩者封裝在獨立的物件中以使它們可以各自獨立地改變和複用;
  • 當一個物件的改變需要同時改變其他物件,而不知道具體有多少物件有待改變;
  • 當一個物件必須通知其他物件,而它又不能假定其他物件是誰。換言之,你不希望這些物件是緊密耦合的;

優點

  • 目標和觀察者間的抽象耦合。一個目標所知道的僅僅是它有一系列觀察者,每個都符合抽象的Observer類的簡單介面。目標不需要知道任何一個觀察者屬於哪一個具體的類。
  • 支援廣播通訊。目標發現的通知不需要指定它的接收者。目標物件並不關心有多少觀察者物件對自己感興趣,唯一的職責就是通知已註冊的各觀察者。

缺點

  • 意外的更新。因為一個觀察者並不知道其他觀察者的存在,它可能對改變目標的最終代價一無所知。在目標上一個看似無害的操作可能會引起一系列對觀察者以及依賴於這些觀察者的那些物件的更新。由此引發的問題常常難以追蹤。

相關模式

  • Mediator:通過封裝複雜的更新語義,ChangeManager充當目標和觀察者之間的中介者。
  • Singleton:ChangeManager可使用單例模式來保證它是唯一的並且是可全域性訪問的。

State(狀態)

意圖

允許一個物件在其內部狀態改變時改變它的行為。物件看起來似乎修改了它的類。

結構

狀態模式包含以下角色:

  • Context(環境類):環境類又稱為上下文類,它是擁有多種狀態的物件。由於環境類的狀態存在多樣性且在不同狀態下物件的行為有所不同,因此將狀態獨立出去形成單獨的狀態類。在環境類中維護一個抽象狀態類State的例項,這個例項定義當前狀態,在具體實現時,它是一個State子類的物件。
  • State(抽象狀態類):它用於定義一個介面以封裝與環境類的一個特定狀態相關的行為,在抽象狀態類中宣告瞭各種不同狀態對應的方法,而在其子類中實現類這些方法,由於不同狀態下物件的行為可能不同,因此在不同子類中方法的實現可能存在不同,相同的方法可以寫在抽象狀態類中。
  • ConcreteState(具體狀態類):它是抽象狀態類的子類,每一個子類實現一個與環境類的一個狀態相關的行為,每一個具體狀態類對應環境的一個具體狀態,不同的具體狀態類其行為有所不同。

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

示例

  // 賬戶有幾種狀態:正常,透支,受限

  // 賬戶類,代表狀態模式中的環境
  class Account {
    private name: string;
    private state: State;
    // 餘額
    private balance = 0;
    // 初始時為正常狀態
    constructor(name: string) {
      this.name = name;
      this.state = new NormalState(this);
      console.log(`使用者 ${this.name} 開戶,餘額為 ${this.balance}`);
      console.log('--------');
    }
    getBalance(): number {
      return this.balance;
    }
    setBalance(balance: number) {
      this.balance = balance;
    }
    setState(state: State) {
      this.state = state;
    }
    // 存款
    deposit(amount: number) {
      this.state.deposit(amount);
      console.log(`存款 ${amount}`);
      console.log(`餘額為 ${this.balance}`);
      console.log(`賬戶狀態為 ${this.state.getName()}`);
      console.log('--------');
    }
    // 取款
    withdraw(amount: number) {
      this.state.withdraw(amount);
      console.log(`取款 ${amount}`);
      console.log(`餘額為 ${this.balance}`);
      console.log(`賬戶狀態為 ${this.state.getName()}`);
      console.log('--------');
    }
    // 結算利息
    computeInterest() {
      this.state.computeInterest();
    }
  }

  // 狀態抽象類
  abstract class State {
    private name: string;
    protected acc: Account;
    constructor(name: string) {
      this.name = name;
    }
    getName() {
      return this.name;
    }
    abstract deposit(amount: number);  
    abstract withdraw(amount: number);  
    abstract computeInterest();  
    abstract stateCheck();
  }

  // 正常狀態類
  class NormalState extends State {
    acc: Account;
    constructor(acc: Account) {
      super('正常');
      this.acc = acc;
    }
    deposit(amount: number) {
      this.acc.setBalance(this.acc.getBalance() + amount);
      this.stateCheck();
    }
    withdraw(amount: number) {
      this.acc.setBalance(this.acc.getBalance() - amount);  
      this.stateCheck();
    }
    computeInterest() {
      console.log('正常狀態,無須支付利息');
    }
    // 狀態轉換
    stateCheck() {
      if (this.acc.getBalance() > -2000 && this.acc.getBalance() <= 0) {  
          this.acc.setState(new OverdraftState(this.acc));  
      } else if (this.acc.getBalance() == -2000) {  
          this.acc.setState(new RestrictedState(this.acc));  
      } else if (this.acc.getBalance() < -2000) {  
          console.log('操作受限');  
      }
    }
  }

  // 透支狀態
  class OverdraftState extends State {
    acc: Account;
    constructor(acc: Account) {
      super('透支');
      this.acc = acc;
    }
    deposit(amount: number) {
      this.acc.setBalance(this.acc.getBalance() + amount);
      this.stateCheck();
    }
    withdraw(amount: number) {
      this.acc.setBalance(this.acc.getBalance() - amount);  
      this.stateCheck();
    }
    computeInterest() {
      console.log('計算利息');
    }
    // 狀態轉換
    stateCheck() {
      if (this.acc.getBalance() > 0) {
        this.acc.setState(new NormalState(this.acc));
      } else if (this.acc.getBalance() == -2000) {
        this.acc.setState(new RestrictedState(this.acc));
      } else if (this.acc.getBalance() < -2000) {
        console.log('操作受限');
      }
    }
  }

  // 受限狀態
  class RestrictedState extends State {
    acc: Account;
    constructor(acc: Account) {
      super('受限');
      this.acc = acc;
    }
    deposit(amount: number) {
      this.acc.setBalance(this.acc.getBalance() + amount);
      this.stateCheck();
    }
    withdraw(ammount: number) {
      console.log('賬號受限,取款失敗');
    }
    computeInterest() {
      console.log('計算利息');
    }
    // 狀態轉換
    stateCheck() {
      if (this.acc.getBalance() > 0) {  
        this.acc.setState(new NormalState(this.acc));  
      } else if (this.acc.getBalance() > -2000) {  
        this.acc.setState(new OverdraftState(this.acc));  
      }
    }
  }

  function stateDemo() {
    const acc = new Account('Bob');
    acc.deposit(1000);  
    acc.withdraw(2000);  
    acc.deposit(3000);  
    acc.withdraw(4000);  
    acc.withdraw(1000);  
    acc.computeInterest(); 
  }
複製程式碼

適用場景

  • 一個物件的行為取決於它的狀態,並且它必須在執行時刻根據狀態改變它的行為;
  • 一個操作中含有龐大的多分支的條件語句,且這些分支依賴於該物件的狀態。這個狀態通常用一個或多個列舉常量表示。有多個操作包含這一相同的條件結構。狀態模式將每一個條件分支放入一個獨立的類中。這使得你可以根據物件自身的情況將物件的狀態作為一個物件,這一物件可以不依賴於其他物件而獨立變化。

優點

  • 封裝了狀態的轉換規則,在狀態模式中可以將狀態的轉換程式碼封裝在環境類或者具體狀態類中,可以對狀態轉換程式碼進行集中管理,而不是分散在一個個業務方法中。
  • 將所有與某個狀態有關的行為放到一個類中,只需要注入一個不同的狀態物件即可使環境物件擁有不同的行為。
  • 允許狀態轉換邏輯與狀態物件合成一體,而不是提供一個巨大的條件語句塊,狀態模式可以讓我們避免使用龐大的條件語句來將業務方法和狀態轉換程式碼交織在一起。
  • 可以讓多個環境物件共享一個狀態物件,從而減少系統中物件的個數。

缺點

  • 狀態模式的使用必然會增加系統中類和物件的個數,導致系統執行開銷增大。
  • 狀態模式的結構與實現都較為複雜,如果使用不當將導致程式結構和程式碼的混亂,增加系統設計的難度。
  • 狀態模式對“開閉原則”的支援並不太好,增加新的狀態類需要修改那些負責狀態轉換的原始碼,否則無法轉換到新增狀態;而且修改某個狀態類的行為也需修改對應類的原始碼。

相關模式

  • 享元模式解釋了何時以及怎樣共享狀態物件;
  • 狀態物件通常是單例;

Strategy(策略模式)

意圖

定義一系列的演算法,把它們一個個封裝起來,並且使它們可相互替換。本模式使得演算法可獨立於使用它的客戶而變化。

結構

策略模式包含以下角色:

  • Context(環境類):環境類是使用演算法的角色,它在解決某個問題(即實現某個方法)時可以採用多種策略。在環境類中維持一個對抽象策略類的引用例項,用於定義所採用的策略。
  • Strategy(抽象策略類):它為所支援的演算法宣告瞭抽象方法,是所有策略類的父類,它可以是抽象類或具體類,也可以是介面。環境類通過抽象策略類中宣告的方法在執行時呼叫具體策略類中實現的演算法。
  • ConcreteStrategy(具體策略類):它實現了在抽象策略類中宣告的演算法,在執行時,具體策略類將覆蓋在環境類中定義的抽象策略類物件,使用一種具體的演算法實現某個業務處理。

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

示例

// 火車票類:環境類
class TrainTicket {
  private price: number;
  private discount: Discount;
  constructor(price: number) {
    this.price = price;
  }
  setDiscount(discount: Discount) {
    this.discount = discount;
  }
  getPrice(): number {
    return this.discount.calculate(this.price);
  }
}

// 折扣介面
interface Discount {
  calculate(price: number): number;
}

// 學生票折扣
class StudentDiscount implements Discount {
  calculate(price: number): number {
    console.log('學生票打7折');
    return price * 0.7;
  }
}

// 兒童票折扣
class ChildDiscount implements Discount {
  calculate(price: number): number {
    console.log('兒童票打5折');
    return price * 0.5;
  }
}

// 軍人票折扣
class SoldierDiscount implements Discount {
  calculate(price: number): number {
    console.log('軍人免票');
    return 0;
  }
}

function strategyDemo() {
  const ticket: TrainTicket = new TrainTicket(100);

  // 從環境中獲取到身份資訊,然後根據身份資訊獲取折扣策略
  const discount: Discount = getIdentityDiscount();
  // 注入折扣策略物件
  ticket.setDiscount(discount);
  // 根據策略物件獲取票價
  console.log(ticket.getPrice());
}
複製程式碼

適用場景

  • 許多相關的類僅僅是行為有異。“策略”提供了一種用多個行為中的一個行為來配置一個類的方法。
  • 需要使用一個演算法的不同變體。
  • 演算法使用客戶不應該知道的資料。可使用策略模式以避免暴露覆雜的、與演算法有關的資料結構。
  • 一個類定義了多種行為,並且這些行為在這個類的操作中以多個條件語句的形式出現。將相關的條件分支移入它們各自的策略類中以代替這些條件語句。

優點

  • 提供了對“開閉原則”的完美支援,使用者可以在不修改原有系統的基礎上選擇演算法或行為,也可以靈活地增加新的演算法或行為。
  • 提供了管理相關的演算法族的辦法。策略類的等級結構定義了一個演算法或行為族,恰當使用繼承可以把公共的程式碼移到抽象策略類中,從而避免重複的程式碼。
  • 提供了一種可以替換繼承關係的辦法。如果不使用策略模式,那麼使用演算法的環境類就可能會有一些子類,每一個子類提供一種不同的演算法。但是,這樣一來演算法的使用就和演算法本身混在一起,不符合“單一職責原則”,決定使用哪一種演算法的邏輯和該演算法本身混合在一起,從而不可能再獨立演化;而且使用繼承無法實現演算法或行為在程式執行時的動態切換。
  • 使用策略模式可以避免多重條件選擇語句。多重條件選擇語句不易維護,它把採取哪一種演算法或行為的邏輯與演算法或行為本身的實現邏輯混合在一起,將它們全部硬編碼在一個龐大的多重條件選擇語句中,比直接繼承環境類的辦法還要原始和落後。
  • 提供了一種演算法的複用機制,由於將演算法單獨提取出來封裝在策略類中,因此不同的環境類可以方便地複用這些策略類。

缺點

  • 客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。這就意味著客戶端必須理解這些演算法的區別,以便適時選擇恰當的演算法。換言之,策略模式只適用於客戶端知道所有的演算法或行為的情況。
  • 策略模式將造成系統產生很多具體策略類,任何細小的變化都將導致系統要增加一個新的具體策略類。
  • 無法同時在客戶端使用多個策略類,也就是說,在使用策略模式時,客戶端每次只能使用一個策略類,不支援使用一個策略類完成部分功能後再使用另一個策略類來完成剩餘功能的情況。

相關模式

  • 享元: 策略物件經常是很好的輕量級物件。

Template Method(模板方法)

意圖

定義一個操作中的演算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟。

結構

模板方法包含以下角色:

  • AbstractClass(抽象類):在抽象類中定義了一系列基本操作(PrimitiveOperations),這些基本操作可以是具體的,也可以是抽象的,每一個基本操作對應演算法的一個步驟,在其子類中可以重定義或實現這些步驟。同時,在抽象類中實現了一個模板方法(Template Method),用於定義一個演算法的框架,模板方法不僅可以呼叫在抽象類中實現的基本方法,也可以呼叫在抽象類的子類中實現的基本方法,還可以呼叫其他物件中的方法。
  • ConcreteClass(具體子類):它是抽象類的子類,用於實現在父類中宣告的抽象基本操作以完成子類特定演算法的步驟,也可以覆蓋在父類中已經實現的具體基本操作。

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

示例

模板方法是基於繼承的一種模式。

下面是一個元件渲染的例子,模擬React元件渲染流程。

// 元件基類
class Component {
  // 模板方法,把元件渲染的流程定義好
  setup() {
    this.componentWillMount();
    this.doRender();
    this.componentDidMount();
  }
  private doRender() {
    // 做實際的渲染工作
  }
  componentWillMount() {}
  componentDidMount() {}
}

class ComponentA extends Component {
  componentWillMount() {
    console.log('A元件即將被渲染');
  }
  componentDidMount() {
    console.log('A元件渲染完成');
  }
}

class ComponentB extends Component {
  componentWillMount() {
    console.log('B元件即將被渲染');
  }
  componentDidMount() {
    console.log('B元件渲染完成');
  }
}

// 渲染A和B元件,生命週期的流程都是相同的,已經在模板方法裡定義好了的
function templateMethodDemo() {
  const compA = new ComponentA();
  compA.setup();

  const compB = new ComponentB();
  compB.setup();
}
複製程式碼

適用場景

  • 需要控制流程的邏輯順序時。模板方法模式廣泛應用於框架設計中,以確保通過父類來控制處理流程的邏輯順序(如框架的初始化,測試流程的設定等)

優點

  • 在父類中形式化地定義一個演算法,而由它的子類來實現細節的處理,在子類實現詳細的處理演算法時並不會改變演算法中步驟的執行次序。
  • 模板方法模式是一種程式碼複用技術,它在類庫設計中尤為重要,它提取了類庫中的公共行為,將公共行為放在父類中,而通過其子類來實現不同的行為,它鼓勵我們恰當使用繼承來實現程式碼複用。
  • 可實現一種反向控制結構,通過子類覆蓋父類的鉤子方法來決定某一特定步驟是否需要執行。
  • 在模板方法模式中可以通過子類來覆蓋父類的基本方法,不同的子類可以提供基本方法的不同實現,更換和增加新的子類很方便,符合單一職責原則和開閉原則。

缺點

  • 需要為每一個基本方法的不同實現提供一個子類,如果父類中可變的基本方法太多,將會導致類的個數增加,系統更加龐大,設計也更加抽象,此時,可結合橋接模式來進行設計。

相關模式

  • 工廠方法: 常被模板方法呼叫。
  • 策略模式:模板方法使用繼承來改變演算法的一部分。策略模式使用委託來改變整個演算法。

訪問者模式

意圖

提供一個作用於某物件結構中的各元素的操作表示,它使我們可以在不改變各元素的類的前提下定義作用於這些元素的新操作。

結構

訪問者模式包含以下角色:

  • Vistor(抽象訪問者):抽象訪問者為物件結構中每一個具體元素類ConcreteElement宣告一個訪問操作,從這個操作的名稱或引數型別可以清楚知道需要訪問的具體元素的型別,具體訪問者需要實現這些操作方法,定義對這些元素的訪問操作。
  • ConcreteVisitor(具體訪問者):具體訪問者實現了每個由抽象訪問者宣告的操作,每一個操作用於訪問物件結構中一種型別的元素。
  • Element(抽象元素):抽象元素一般是抽象類或者介面,它定義一個accept()方法,該方法通常以一個抽象訪問者作為引數。【稍後將介紹為什麼要這樣設計。】
  • ConcreteElement(具體元素):具體元素實現了accept()方法,在accept()方法中呼叫訪問者的訪問方法以便完成對一個元素的操作。
  • ObjectStructure(物件結構):物件結構是一個元素的集合,它用於存放元素物件,並且提供了遍歷其內部元素的方法。它可以結合組合模式來實現,也可以是一個簡單的集合物件,如一個List物件或一個Set物件。

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

示例

一個公司有兩種員工,正式工和臨時工,他們有不同的工時和薪酬結算方法。

  // 員工介面
  interface Employee {
    accept(handler: Department): void;
  }

  // 全職員工類
  class FulltimeEmployee implements Employee {
    private name = '';
    // 全職員工按週薪計算薪酬
    private weeklyWage = 0;
    // 工作時長
    private workTime = 0;
    constructor(name: string, weeklyWage: number, workTime: number) {
      this.name = name;
      this.weeklyWage = weeklyWage;
      this.workTime = workTime;
    }
    getName(): string {
      return this.name;
    }
    getWeeklyWage(): number {
      return this.weeklyWage;
    }
    getWorkTime(): number {
      return this.workTime;
    }
    // 實現介面,呼叫訪問者處理全職員工的方法
    accept(handler: Department) {
      handler.visitFulltime(this);
    }
  }

  // 臨時員工類
  class ParttimeEmployee implements Employee {
    private name = '';
    // 臨時員工按時薪計算薪酬
    private hourWage = 0;
    // 工作時長
    private workTime = 0;
    constructor(name: string, hourWage: number, workTime: number) {
      this.name = name;
      this.hourWage = hourWage;
      this.workTime = workTime;
    }
    getName(): string {
      return this.name;
    }
    getHourWage(): number {
      return this.hourWage;
    }
    getWorkTime(): number {
      return this.workTime;
    }
    // 實現介面,呼叫訪問者處理臨時工的方法
    accept(handler: Department) {
      handler.visitParttime(this);
    }
  }

  // 部門介面
  interface Department {
    visitFulltime(employee: FulltimeEmployee): void;
    visitParttime(employee: ParttimeEmployee): void;
  }

  // 具體訪問者——財務部,結算薪酬實現部門介面
  class FADepartment implements Department {
    // 全職員工薪酬計算方式
    visitFulltime(employee: FulltimeEmployee) {
      const name: string = employee.getName();
      let workTime: number = employee.getWorkTime();
      let weekWage: number = employee.getWeeklyWage();
      const WEEK_WORK_TIME = 40;
      if (workTime > WEEK_WORK_TIME) {
        // 計算加班工資
        const OVER_TIME_WAGE = 100;
        weekWage = weekWage + (workTime - WEEK_WORK_TIME) * OVER_TIME_WAGE;
      } else if (workTime < WEEK_WORK_TIME) {
        if (workTime < 0) {
          workTime = 0;
        }
        // 扣款
        const CUT_PAYMENT = 80;
        weekWage = weekWage - (WEEK_WORK_TIME - workTime) * CUT_PAYMENT;
      }
      console.log(`正式員工 ${name} 實際工資為:${weekWage}`);
    }
    // 臨時工薪酬計算方式
    visitParttime(employee: ParttimeEmployee) {
      const name = employee.getName();
      const hourWage = employee.getHourWage();
      const workTime = employee.getWorkTime();
      console.log(`臨時工 ${name} 實際工資為:${hourWage * workTime}`);
    }
  }

  // 具體訪問者——人力資源部,結算工作時間,實現部門介面
  class HRDepartment implements Department {
    // 全職員工工作時間報告
    visitFulltime(employee: FulltimeEmployee) {
      const name: string = employee.getName();
      let workTime: number = employee.getWorkTime();
      // 實際工作時間報告
      let report = `正式員工 ${name} 實際工作時間為 ${workTime} 小時`;
      const WEEK_WORK_TIME = 40;
      if (workTime > WEEK_WORK_TIME) {
        // 加班時間報告
        report = `${report},加班 ${WEEK_WORK_TIME - workTime} 小時`;
      } else if (workTime < WEEK_WORK_TIME) {
        if (workTime < 0) {
          workTime = 0;
        }
        // 請假時間報告
        report = `${report},請假 ${WEEK_WORK_TIME - workTime} 小時`;
      }
      console.log(report);
    }
    // 臨時工工作時間報告
    visitParttime(employee: ParttimeEmployee) {
      const name: string = employee.getName();
      const workTime: number = employee.getWorkTime();
      console.log(`臨時工 ${name} 實際工作時間為 ${workTime} 小時`);
    }
  }

  // 員工集合類
  class EmployeeList {
    list: Array<Employee> = [];
    add(employee: Employee) {
      this.list.push(employee);
    }
    // 遍歷員工集合中的每一個物件
    accept(handler: Department) {
      this.list.forEach((employee: Employee) => {
        employee.accept(handler);
      });
    }
  }

  function visitorDemo() {
    const list: EmployeeList = new EmployeeList();
    const full1 = new FulltimeEmployee('Bob', 3000, 45);
    const full2 = new FulltimeEmployee('Mikel', 2000, 35);
    const full3 = new FulltimeEmployee('Joe', 4000, 40);
    const part1 = new ParttimeEmployee('Lili', 80, 20);
    const part2 = new ParttimeEmployee('Lucy', 60, 15);

    list.add(full1);
    list.add(full2);
    list.add(full3);
    list.add(part1);
    list.add(part2);

    // 財務部計算薪酬
    const faHandler = new FADepartment();
    list.accept(faHandler);

    // 人力資源部出工作報告
    const hrHandler = new HRDepartment();
    list.accept(hrHandler);
  }
複製程式碼

適用場景

  • 一個物件結構包含多個型別的物件,希望對這些物件實施一些依賴其具體型別的操作。在訪問者中針對每一種具體的型別都提供了一個訪問操作,不同型別的物件可以有不同的訪問操作。
  • 需要對一個物件結構中的物件進行很多不同的並且不相關的操作,而需要避免讓這些操作“汙染”這些物件的類,也不希望在增加新操作時修改這些類。訪問者模式使得我們可以將相關的訪問操作集中起來定義在訪問者類中,物件結構可以被多個不同的訪問者類所使用,將物件本身與物件的訪問操作分離。
  • 物件結構中物件對應的類很少改變,但經常需要在此物件結構上定義新的操作。

優點

  • 增加新的訪問操作很方便。使用訪問者模式,增加新的訪問操作就意味著增加一個新的具體訪問者類,實現簡單,無須修改原始碼,符合“開閉原則”。
  • 將有關元素物件的訪問行為集中到一個訪問者物件中,而不是分散在一個個的元素類中。類的職責更加清晰,有利於物件結構中元素物件的複用,相同的物件結構可以供多個不同的訪問者訪問。
  • 讓使用者能夠在不修改現有元素類層次結構的情況下,定義作用於該層次結構的操作。

缺點

  • 增加新的元素類很困難。在訪問者模式中,每增加一個新的元素類都意味著要在抽象訪問者角色中增加一個新的抽象操作,並在每一個具體訪問者類中增加相應的具體操作,這違背了“開閉原則”的要求。
  • 破壞封裝。訪問者模式要求訪問者物件訪問並呼叫每一個元素物件的操作,這意味著元素物件有時候必須暴露一些自己的內部操作和內部狀態,否則無法供訪問者訪問。

相關模式

  • 組合模式:訪問者可以用於對一個由組合模式定義的物件結構進行操作;

參考文件

本文介紹了最後5種物件行為型模式,多謝大家對於系列文章的支援~對團隊感興趣的同學可以關注專欄或者傳送簡歷至'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~

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

相關文章