Design Pattern | 從復仇者看策略模式( Strategy Pattern ) feat. TypeScript
前言
Hi!大家好,我是神 Q 超人。其實設計模式是我一直想要講的主題,從一開始看到書的時候,就一直想講,但要補的基礎實在是滿多的,所以才會拖到現在才生出第一篇,好不容易讓我抓到機會,趁著前幾天剛講完 Class 和 Interface ,就來看看它們能在 Design Pattern 中做出怎樣的精彩共演吧!
Strategy Pattern 策略模式
Strategy Pattern 主要在處理各種既相同,但做起來又不太一樣的行為。將行為抽離後,視情況隨意組合切換。
將行為抽離後,視情況隨意組合切換。
設計時的狀況情境
下方先介紹造成系統設計變得複雜的例子,最後再展示 Strategy Pattern 是如何優雅解決這個問題。
上方提到既相同又不太一樣的行爲似乎有點矛盾,但是仔細想想,就像「飛」這個動作好了,索爾、鋼鐵人、幻視、蜘蛛人都會飛,但是它們飛的方法都不一樣對吧?
也許大家可以想到將「飛的行為」寫到 Inheritance (繼承) TheAvengers
的 Child Class (子類別)中,例如索爾、鋼鐵人等等…但是
這個「飛」的行為實現就會重複分佈在各個 Child Class (子類別)中。
這麼做顯然不是很好維護,因此你又將「飛」的主要行為「脫離地心引力」給提到了「復仇者」這個 Parent Class (父類別),然後把每個人實現「飛」的方式用 Inheritance 做 Override ,但是
你絕對不會想看見浩克在天上飛對吧?
於是你開始檢查繼承了 TheAvengers
的所有 Child Class ,找到了浩克、黑寡婦,等等不會飛的復仇者,將它們的「飛」寫成不做任何事的空 Function ,來 Override 在 Parent Class 中的行為,像這樣子:
class Hulk extends TheAvengers {
public fly():void {
}
}
天啊!看著這些 Code 你心裡會有什麼想法?但身為工程師的直覺正告訴你,「絕對不會是這麼一回事。」對吧?於是 Google 後,你的瞳孔映射出的 Strategy Pattern 正閃著光芒。
Strategy Pattern 實作流程
有解決問題的方法,那就一定會有解決的流程,以下將解決的流程分成幾項。
找出實作不同的相同行為
以復仇者的例子來說,相同的行為是「飛」,於是我們將「飛」給抽出來,設計成 Interface ,並實作出幾種 Class ,代表各種「達成行為的方式」:
interface IFlyBehavior {
fly(name: string): void;
}
class CanNotFly implements IFlyBehavior {
public fly(name: string): void {
console.log(`${name} 不會飛`);
}
}
class FlyWithHammer implements IFlyBehavior {
public fly(name: string): void {
console.log(`${name} 用錘子飛`);
}
}
class FlyWithArmor implements IFlyBehavior {
public fly(name: string): void {
console.log(`${name} 穿著鋼鐵裝飛`);
}
}
在 Parent Class 中透過 Interface 執行行為
將飛的方式抽成 Interface ,並用不同的 Class 實現後,要能夠在 Parent Class 中去執行,因此得將實現行為的 Class 放進 Parent Class 的屬性裡。
這時候接口就派上用場了,因為不論是哪種飛的方式,都是 IFly
的實現,所以可以透過 IFly
定義屬性的型別,再直接以該接口執行行為:
class TheAvengers {
public name: string;
private flyBehavior: IFlyBehavior;
constructor(name: string, flyBehavior: IFlyBehavior) {
this.name = name;
this.flyBehavior = flyBehavior;
}
public fly(): void {
this.flyBehavior.fly(this.name);
}
}
上方的第 4 行就是定義 flyBehavior
屬性,並將它的型別指定為接口 IFlyBehavior
,如此一來不論是實現何種飛的 Class ,都可以在 constructor
時放進 flyBehavior
中。
第 11 行的 fly()
裡沒有任何「飛的邏輯」在裡面,而是透過 flyBehavior
去執行「飛」。
最後還有繼承了 TheAvengers
的 Child Class ,它們除了要處理各自的屬性外,還要在 Constructor 中將剛剛建立的行為給 TheAvengers
:
// 下方三個 Child Class 繼承了 TheAvengers ,並將飛的行為用 super 送給 TheAvengers
class Hulk extends TheAvengers {
constructor() {
super('浩克', new CanNotFly());
}
}
class Thor extends TheAvengers {
constructor() {
super('索爾', new FlyWithHammer());
}
}
class IronMan extends TheAvengers {
constructor() {
super('鋼鐵人', new FlyWithArmor());
}
}
// 建立 instance
const hulk = new Hulk();
const thor = new Thor();
const ironMan = new IronMan();
// 各自執行飛的動作
theHulk.fly(); // 浩克 不會飛
thor.fly(); // 索爾 用錘子飛
ironMan.fly(); // 穿著鋼鐵人裝飛
注意哦!這時候所有的 fly
行為都是委託 IFly
接口執行,在 TheAvengers
裡根本就不需要知道是怎麼辦到的,
只要知道它接收了 IFly 接口類型,一定會有這個 fly Method 存在,不論他到底會做什麼,把執行飛的邏輯與 TheAvengers 拆開,只透過接口執行,就是把行為給封裝起來,
封裝後再利用組合的方式處理不同的飛行方式,因為每個飛行方式都是一個實現了 IFly
的獨立 Class ,所以說,如果當我在 TheAvengers
中再加上 setFlyBehavior
這個 Method ,重新設定 flyBehavior
屬性:
class TheAvengers {
/*其餘省略*/
public setFlyBehavior(flyBehavior: IFlyBehavior): void {
this.flyBehavior = flyBehavior;
}
}
這麼一來,當有天浩克在路上撿到索爾的錘子,那他也就飛的起來:
const hulk = new Hulk();
hulk.fly(); // 浩克 不會飛
// 重新對浩克設定飛的行為
hulk.setFlyBehavior(new FlyWithHammer());
hulk.fly(); // 浩克 用錘子飛
結論
以下整理幾點關於 Strategy Pattern 的好處:
- 所有用不同方式實現的同一行為,都可以藉由 Interface 定義型別,所以不會綁定某個 Class ,造成耦合性太高。
- 因為是用 Interface 的關係,所以不論遇到什麼樣的狀況,都能夠自由切換不同的實現方式,只要它也是同一個 Interface 的實現。
- 比起用 Inheritance 控制,將行為抽出後便能統一管理各個實現行為的邏輯,而不是讓這些邏輯重複且分散在各個 Child Class 中。
- 因為行為被封裝起來了,所以 Class 只需要針對 Interface 做處理,而不是那些實現的行為。
如果大家看完文章,對 Strategy Pattern 有興趣的話,可以到筆者練習的 GitHub ,裡面有很多在練習時到處玩的例子,可以一起交流!
本篇文章打起來的感覺很新奇,因為第一次不是介紹作法,而是講解怎麼應用,因此在開始時試著用一些情境,讓大家了解在開發時遇到的問題,再帶出 Design Pattern ,如果文章中有任何可以改進的地方、或是問題,再麻煩留言告訴我,感激不盡 🙇。
參考文章