Typescript玩轉設計模式 之 對象行為型模式(下)

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

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

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

Observer(觀察者)

意圖

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

結構

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

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

示例

推模型

目標向觀察者發送關於改變的「詳細信息」,而不管它們需要與否。由目標維護觀察者。

// 場景:顧客點菜後,服務員記下顧客的信息,菜做好後廣播通知顧客領取 // 觀察者基類 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(具體狀態類):它是抽象狀態類的子類,每一個子類實現一個與環境類的一個狀態相關的行為,每一個具體狀態類對應環境的一個具體狀態,不同的具體狀態類其行為有所不同。

示例

// 賬戶有幾種狀態:正常,透支,受限 // 賬戶類,代表狀態模式中的環境 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(具體策略類):它實現了在抽象策略類中聲明的演算法,在運行時,具體策略類將覆蓋在環境類中定義的抽象策略類對象,使用一種具體的演算法實現某個業務處理。

示例

// 火車票類:環境類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(具體子類):它是抽象類的子類,用於實現在父類中聲明的抽象基本操作以完成子類特定演算法的步驟,也可以覆蓋在父類中已經實現的具體基本操作。

示例

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

下面是一個組件渲染的例子,模擬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對象。

示例

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

// 員工介面 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(####, @),歡迎有志之士加入~

原文地址:juejin.im/post/5a77211b

推薦閱讀:

ThinkJS 3.0 如何實現對 TypeScript 的支持
如何看待 Angular 2.0 使用的 AtScript 是 TypeScript 的超集?
如何看待Google和Microsoft在Angular JS 2 和 TypeScript上的合作?
【認真臉】註解與裝飾器的點點滴滴
angular 和 typescript 到底是否適合最佳實踐?

TAG:前端開發 | 設計模式 | TypeScript |