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

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

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

Chain of Responsibility(職責鏈)

意圖

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

結構

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

  • 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); }}

宏命令

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

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實例。

示例

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

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

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

推薦閱讀:

如何看待Google和Microsoft在Angular JS 2 和 TypeScript上的合作?
【1-2】開發語言
VS Code 1.14更新日誌
【認真臉】註解與裝飾器的點點滴滴
angular 和 typescript 到底是否適合最佳實踐?

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