--- tags: aliases: date: 2022-05-26 time: 15:12:04 description: --- ## 策略模式(Strategy) 策略模式可以提供不一樣的演算法,但是又不用更改程式。 以常見的鴨子為例,有一個基礎類別Duck,如何衍生出會飛得鴨子跟不會飛的鴨子?抑或是會叫的跟不會叫的? 第一部是將會變動的部份分離出來,讓鴨子類別不需要去在乎飛跟叫的問題。 再來是把飛跟叫的部份包裝成另一個class,並以之為基礎類別來實做出「實際的類別」。 ^e59e9f 以一般C++的override方法,會用的方式大致是這樣: - !!!col - 1 ### 基礎類別 ```cpp class duck { duck() {} void fly() {} void makeSound() {} void run() {} } ``` - 1 ### 衍生類別,遙控鴨子,會飛不會叫 ```cpp class duckRC : public duck { duckRC() {} void fly() { printf("I can fly"); } void makeSound() { printf("I cannot make sound!"); } } ``` - 1 ### 衍生類別,木頭鴨子,不會飛不會叫 ```cpp class duckWood : public duck { duckWood() {} void fly() { printf("I cannot fly"); } void makeSound() { printf("I cannot make sound!"); } } ``` 但是這樣的話如果基礎類別會更改的話,那麼子類別也全部都會受影響,例如,現在希望所有的鴨子都要有一個「跑」的功能,所以我們就在基礎類別裡加一個`run()`: ```cpp class duck { void fly() {} void makeSound() {} void run() { printf("I can run"); } } ``` 結果現在木頭鴨子也能跑了,這不符合我們的設計,所以我們必須回頭更改木頭鴨子的程式,改成 ```cpp class duckWood : public duck { void fly() { printf("I cannot fly"); } void makeSound() { printf("I cannot make sound!"); } void run() { printf("I cannot run!"); } // <- 要改! } ``` 如果我們類別很多,那麼就很可能有沒改到的漏網之魚,bug就發生了。 ### 封裝變動的部份 比較好的方法一旦類別寫完之後,我們就不要動它了,日後新增的功能也不可以影響到他。我們把「會變動」的部份分離出來,變成各自的類別。 為了簡化,我們討論「飛」這個功能就好,「飛」只分成「會飛」跟「不會飛」2種類別: - !!!col - 1 ### 基礎類別,IFly ```cpp class IFly { void doFly() = 0; } ``` - 1 ### 衍生類別,CanFly ```cpp class CanFly { void doFly() { printf("I can fly"); } } ``` - 1 ### 衍生類別,CannotFly ```cpp class CannotFly { void doFly() { printf("I cannot fly"); } } ``` 回到鴨子的基礎類別這邊,基礎類別`duck`本來是直接擁有`fly()`這個member function,現在我們把他改成他擁有的是`IFly`: ```cpp class duck { duck() {} fly() { fly->doFly() }; IFly* fly = nullptr; ... } ``` 重寫遙控鴨子這個衍生類別: ```cpp class duckRC : public duck { duckRC() { this->fly = new CanFly(); } ... } ``` 重寫木頭鴨子,不會飛,所以: ```cpp class duckWood : public duck { duckWood() { this->fly = new CannotFly(); } } ``` 現在,不管是遙控鴨子或是木頭鴨子,在被呼叫`fly()`的時候,都是根據它fly的「實際類別」來動作,遙控鴨子的`fly()`呼叫的是`CanFly`的`doFly()`,木頭鴨子的`fly()`呼叫的是`CannotFly`的`doFly()`,它們的動作完全取決於他們初始化的方式,而他們初始化的方式則取決於你的產品定義,要是今天你需要不同的飛行方式,增加新的fly類別即可,不會影響到舊有的。要是鴨子基礎類別增加了新的行為,如run,那麼就幫鴨子基礎類別的`run()`加個什麼都不做的預設動作就好,反正舊有的鴨子衍生類別本來就對這個新行為沒反應。要是`CanFly`的定義改變了,那麼你就是改變了使用`CanFly`的所有類別,這是你的定義明確的改變了,不是有程式被「額外」的改變了。 - !!!col - 1 ### 原本直接繼承的方式 ![[Pasted image 20220526182952.png]] - 2 ### 封裝變動的部份 ![[Pasted image 20220526183019.png]] 這樣做的另一個好處是fly的初始化是動態的,只要再多一個`set()` function就可以動態的切換實作,也就是說你可以從設定檔來決定你的鴨子要長什麼樣子。 ## 觀察者模式(Observer Pattern ) 有一個會產生變動的主角(subject),與一堆需要觀察變動的觀察者(Observer)。觀察者向主角註冊,當主角發生變化的時候,發後通知給觀察者。 ![[20220614154819_Observer_Pattern.png]] Subject方面,attach() 就是註冊觀察者,也可以叫做 register()、add() 之類。 detach() 用來移除用戶,也可以叫做 unregister()、remove() 之類。 notify()則是當發生變化時,用來通知所有觀察者的實作。 觀察者方面必須實作 update() 才能收到通知。 ## 裝飾者模式(Decorator Pattern) 「裝飾者」通常與「被裝飾者」有同樣的界面,「裝飾者」會取代「被裝飾者」的界面,進而改變「被裝飾者」的行為。 裝飾者模式讓物件可以動態的改變行為,進為符合不同的需求。 以書上的例子來說,我們多種飲料,每種飲料都可以加上不同的配料。例如,有奶茶、綠茶、紅茶3種飲料,另外有珍珠、紅豆、綠豆、仙草4種配料,我們要如何設計出適合的類別來讓每種飲料都可以隨寄的搭配配料呢? 假設這樣寫: - !!!col - 1 ### Ingredient class ```cpp class IngredientBubble { ... }; class IngredientRedbean { ... }; class IngredientGreenbean { ... }; class IngredientFairyGrass { ... }; ``` - 1 ### Beverage class ```cpp class Beverage { int32_t cost() { return cost; } int32_t cost; } class BeverageMilkTea : public Beverage { ... }; class BeverageGreenTea : public Beverage { ... }; class BeverageBlackTea : public Beverage { IngredientBubble* bubble; IngredientRedbean* redbean; IngredientGreenbean* greenbean; IngredientFairyGrass* fairyGrass; } ``` 每個飲料的class裡面都將每個配料定義為一個 member,如果客人有加配料的話,我們就將配料實例化,假設奶茶加了珍珠: ```cpp class BeverageBlackTea { void addBubble() { if (!bubble) bubble = new IngredientBubble(); } }; ``` 要算價格的時候: ```cpp class BeverageBlackTea { int32_t cost() { if (bubble) cost += 10; // 珍珠要加10元 if (redbean) cost += 5; // 紅豆要加10元 if (greenbean) cost += 7; // 綠豆要加10元 if (fairyGrass) cost += 9; // 珍珠要加10元 return cost; } int32_t cost = 30; // 奶茶本身10元 }; ``` 這樣的問題是,每當有一種新配料出現,我們就要在奶茶類別裡修改至少2個 function:`addXXX()` 與 `cost()`,目前我們有3種飲料,所以要修改6個 function,更何況,如果客人要加2份珍珠怎麼辦?這明顯不利程式的維護,必須有一種方法讓程式的修改最小,讓寫好的程式不用被修改才行。 讓裝飾者模式來改善這個問題。首先讓配料跟飲料有同樣的界面,但是修改一下配料的 constructor,當然也要實作 `cost()`,畢竟每個配料的價格不同: ```cpp class IngredientBubble : public Beverage { IngredientBubble(Beverage* beverage) { this->beverage = beverage; } int32_t cost() { return 10 + this->beverage->cost(); // 珍珠要加10元,在加上原本飲料的價錢 } Beverage* beverage; }; ``` 珍珠這個 class 的 constructor 的參數是任何一個 Beverage,現在飲料被包在珍珠裡面,由珍珠來決定飲料最後的價格是多少。現在我們可以動態的決定飲料的組成,假設點一杯紅茶,加一些配料: ```cpp int main() { ... // 為了讓程式看起來簡單,這裡就先不考慮memory leak的問題...>_< BeverageBlackTea* berverge = new BeverageBlackTea(); // 點一杯紅茶 IngredientBubble* berverge = new IngredientBubble(berverge); // 加珍珠 IngredientRedbean* berverge = new IngredientRedbean(berverge); // 加紅豆 IngredientFairyGrass* berverge = new IngredientFairyGrass(berverge); // 加仙草 ... return berverge->cost(); // 算價錢 } ``` 可以看到第一個變數是飲料,然後被包珍珠裡面,變成珍珠紅茶,再被包到紅豆裡面,變成紅豆珍珠紅茶,再被包到仙草裡面,變成仙草紅豆珍珠紅茶,到這樣的好處是: 1. 有新的配料就寫新配料的 class 2. 因為可以動態的組合,原本寫好的飲料 class 就不用在去動它了,愈少修改,愈少 bug 3. 我們可以動態的組合配料,要加2份以上也沒有問題