# Design Pattern ( 設計模式 ) ###### tags: `design pattern` `C++` > 設計模式是經過一些大佬們經過實戰後歸納出的寫程式方法, > 善用設計模式,可以讓程式更好被維護。 > 參考自 "**深入淺出的設計模式**",有興趣的話還是買來看一下唄 ~ --- ## :memo: 常見的 patterns > 上班好像會需要用到 \_(´ཀ\`」 ∠)\_,所以就做一下筆記讓自己更清楚,也希望能幫到有需要der人~ > 佛系更新中 ~ --- ## 1. Strategy Pattern 當你可能需要重複使用某些行為 (function),但那些行為可能有幾種變形,甚至有可能在未來新增新的行為變形,此時就可以使用 Strategy Pattern,將這些類似的行為用抽象的類別統合起來。 - 情境 : 你今天想要建立一系列的鴨子類別(有許多種鴨子),因此你先建立了一個鴨子類別 : ```c++=1 class Duck{ public: void perform_quack() = 0; }; ``` 然後你再實作各式各樣的鴨子,而它們當然就會繼承自鴨子類別,並且實作 quack 方法,例如 : ```c++=1 class Redhead_duck : public Duck{ public: void perform_quack(){ // Do something ... std::cout<<"Quack !\n"; } }; class Greenhead_duck : public Duck{ public: void perform_quack(){ // Do something ... std::cout<<"Squeak !\n"; } }; ``` 雖然這樣看起來就完成了,但是當你在未來的某一天又新增了一個新的鴨子種類,例如 : ```c++=1 class New_type_duck : public Duck{ public: void perform_quack(){ // Do something ... std::cout<<"Quack !\n"; } }; ``` 我們會發現,這個鴨子的叫法 (quack) 其實和 Redhead_duck 是一樣的 (假設他們真的是一樣的) , 然而我們卻還是一字不漏地實作了一次 perform_quack,更糟的是,若是未來的某一天,這種叫法 需要被修改 (例如 Quack -> Quack Quack),此時你得翻遍所有的鴨子類別,並且逐一檢查看看你是否需要去修改它 ... 此時我們就會發現,這不是一隻好的程式。 - ... 因此 ! 我們就要來使用 Strategy Pattern,將鴨子的叫法 (quack) 實作成類別,並且同時也使用更高階的抽象類別去代表所有的鳴叫方法 (就像鴨子類別一樣) : ```c++=1 class Quack_behavior{ public: virtual void make_sound() = 0; }; ``` 有了這個抽象的鳴叫行為之後,我們便可以去實作各式各樣的鳴叫方法了 : ```c++=1 class Quack : public Quack_behavior{ public: void make_sound(){ // Do something ... std::cout<<"Quack !\n"; } }; class Squeak : public Quack_behavior{ public: void make_sound(){ // Do something ... std::cout<<"Squeak !\n"; } }; ``` 好的,現在我們有鳴叫的類別了,可是要怎麼把它們和鴨子類別們組合起來呢 ? 就如同字面上的意思,我們將使用"組合"的技巧。 > :+1: 設計法則之一 : 多用組合,少用繼承 ! - 組合 ( Has a ) : 將類別放在另一個類別之中 ( Duck 當中有 Quack_behavior ) ```c++=1 class Duck{ public: void perform_quack(){ _quack_strategy->make_sound(); } Quack_behavior *_quack_strategy = nullptr; }; ``` 現在我們將鴨子類別改頭換面了,我們在鴨子類別中放入了 Quack_behavior 的指標,這個指標未來會指向某一種鳴叫方法,因此我們就可以順便把鴨子的鳴叫行為 ( perform_quack ) 實作出來,因為所有的鳴叫方法都繼承自 Quack_behavior,因此都一定要實作 make_sound 這個 virtual function,而這個 make_sound 則會提供我們不同的叫聲 ( 看你指向哪種鳴叫方法 )。 - 此時我們的各種鴨子類別們就只需要這麼做 : ```c++=1 class Redhead_duck : public Duck{ public: void Redhead_duck(){ _quack_strategy = new Quack; } }; class Greenhead_duck : public Duck{ public: void Greenhead_duck(){ _quack_strategy = new Squeak; } }; ``` - 成果 : 回到一開始的情境,當我們要新增一個新的鴨子種類,我們只要這麼做 : ```c++=1 class New_type_duck : public Duck{ public: void New_type_duck(){ _quack_strategy = new Quack; } }; ``` 一行搞定沒毛病,而且當你未來需要對 Quack 這種鳴叫方式進行修改時,你只需要去找出 Quack 的類別,並且對它進行修改即可,如此一來就能使所有指向 Quack 這種鳴叫方法的鴨子受到影響。 如果你今天需要新增一種新的鳴叫方式也很簡單 : ```c++=1 class New_type_quack : public Quack_behavior{ public: void make_sound(){ std::cout<<"??? \n"; } }; ``` 如此一來就能提供給鴨子們更多種類的鳴叫方法了 ! - 更多好處 : 原本我們在實作鴨子類別時,我們就會順便賦予它鳴叫的方法 ( 指向某種 Quack_behavior ),但是其實我們也可以在 run time 對這著指標進行修改 ( 例如設計一個 set_quack_behavior ),如此一來鴨子的叫聲甚至能在程式中依照需求隨時改變 ! // 2021.10.15 更新 --- ## 2. Observer Pattern 當你的某個物件(O)想要關注另一個物件(S)的動向,也就是說,當物件(S)發生改變時,你希望物件(O)能收到通知,並且得知改變的內容,此時你就該使用觀察者模式 ( Observer Pattern )。 - 情境 : 你最近打算設計一個有關於天氣的app,它會在你的手機上顯示最新的溫度&天氣的資訊,而資訊的來源則是從氣象站的介面取得的。此時大家冒出的第一個念頭大概就是定期主動詢問的方式(例如爬蟲)去抓取最新資訊,然而這會有一些缺點 : 1. 並不是每一次的詢問都能夠"恰恰好",當你詢問的太頻繁,氣象站的資訊根本就還沒更新,這樣等於白做工了;然而降低詢問的頻率,又可能導致無法第一時間發現氣象站的更新。 2. 這種主動式的提問並不在氣象站的預料之內(它並不知道有多少人、會在什麼時候來提問),因此可能會對氣象站造成負擔。 - 假設 : 以上的情況可以說是陷入死胡同了,但是假如我們能夠重新對氣象站的程式進行設計呢 ? 此時我們可以參考一下社群媒體的作法 ( 例如 youtube ),如果我們對某一個 Youtuber 有興趣,那麼我們就去訂閱他,這樣一來我們就能夠在第一時間得知他的最新消息 ( 例如開台 )。 而這個機制我們可以用下面的 UML diagrams 來理解一下 ~ - UML Diagrams of Observer Pattern ```sequence Observer->Subject: 訂閱 (關注他的動向) Note right of Subject: 當狀態發生變化 Subject->Observer: 通知 (有訂閱的觀察者) Note left of Observer: 觀察任務結束 Observer->Subject: 取消訂閱 Note right of Subject: 當狀態發生變化 Subject-->Observer: 不再通知 (只通知有訂閱的) ``` 我們就是 Observer ,而我們有興趣的 Youtuber 則是 Subject,當我們訂閱他之後,當他的狀態發生了變化 ( 開台 ),系統就會主動通知他的每一位訂閱者 ( 就4我們 )。 這種作法不僅省下了我們的時間 ( 不用三不五時就去檢查他有沒有開台 ),也能夠在系統的預料之內, 因為是系統主動發送的。 > :+1: 好萊塢名言(同時也適用於程式設計) : 不要打給我,我會打給你 ! - 成果 : 回到一開始的情境,如果我們能夠改寫氣象站的程式,我們可以這麼做 : ```c++=1 class Observer{ public: virtual void update() = 0; }; class Subject{ public: virtual void register_observer(Observer *O) = 0; virtual void remove_observer(Observer *O) = 0; virtual void notify_observer() = 0; }; ``` 我們先建立兩個抽象類別,分別代表 Observer 跟 Subject,然後設計好它們各自要實作的功能。先來講 Subject,它要實作的功能很直觀,首先是能夠讓人訂閱/退訂閱,再來是要能夠通知訂閱你的人; 而 Observer 這邊,則需要提供一個接收資訊的方式 ( 這裡理論上就是要把天氣資訊傳過去 )。 - 然後我們依據 Subject 的定義重新撰寫了氣象站的程式 : ```c++=1 class Weather_Station : public Subject{ public: void register_observer(Observer *O){ _observers.push_back(O); } void remove_observer(Observer *O){ _observers.remove(O); } void notify_observer(){ for(auto observer : _observers){ // let observer decide how and what to update observer->update(); } } void get_parameter(float temp, float humi, float pres){ _temperature = temp; _humidity = humi; _pressure = pres; std::cout<<"-------------------\n"; notify_observer(); } float get_temperature(){ return _temperature; } float get_humidity(){ return _humidity; } float get_pressure(){ return _pressure; } private: std::list<Observer*> _observers; float _temperature; float _humidity; float _pressure; }; ``` 從程式中可以得知,這個氣象站原本有紀錄三個數據 : 溫度、濕度、氣壓,並且也有提供取得方式,而不同的是,我們加入了第4個數據,就是 \_observers,它是一個記錄著所有觀察者位置的 list。 當有人想要成為觀察者時,就會透過氣象站提供的 register_observer 記錄進 \_observers 裡。 而當氣象站的天氣資訊更改時 ( 這裡假設是 get_parameter ),它就會一併執行 notify_observer, 這個 function 則會通知所有在 \_observers 裡的成員,通知方法就是呼叫 Observer 的 update,而這裡的作法可以是單純的把溫度、濕度、氣壓都當成 input 並傳送過去,但是我們會發現在這份 code 中的 update 是沒有 input 的,因為這麼做能夠讓程式更有彈性 ! - 接著來撰寫我們的天氣 app : ```c++=1 class My_App : public Observer{ public: My_App(Weather_Station* S){ _subject = S; _subject->register_observer(this); } ~My_App(){ _subject->remove_observer(this); } void update(){ if(_subject == nullptr) return; _temperature = _subject->get_temperature(); display(); } void display(){ std::cout<<"Temp : "<<_temperature<<std::endl; } private: Weather_Station* _subject = nullptr; float _temperature; }; ``` 我們這個 app 很偷懶,它在一出生時就去訂閱了氣象站 ( 應該要獨立出來做比較好 ),然後我們會發現,這個 My_App 中竟然也存了一份 Weather_Station 的位置 ! 而這就是為什麼 update 可以不用有 input 了,因為我們可以在 update function 自己決定要跟 Weather_Station 拿什麼天氣資訊 ! > 這算是一種弱耦合的設計方法,也就是 "你中有我,我中有你",但是彼此的影響程度其實不大。 // 2021.10.16 更新 --- ## 3. Decorator Pattern 如果你今天想要建立一種可能會帶有許多形容詞的物件 ( 金黃色的長毛的大型犬 ),而這些形容詞可以產生各式各樣的排列組合,並且時常被重複使用,那麼你就可以考慮將這些形容詞做成裝飾器模式。 - 情境 : 你最近去某義大利麵店擔任工讀生,並負責點餐的服務,然後你發現義大利麵的品項實在是多到爆 ! 經過仔細觀察之後,你發現品項會這麼多的原因是因為"乘法放大",例如說麵的種類有直麵、斜管麵、螺旋麵,然後肉類有蛤蜊、培根、雞排,而醬汁有紅醬、白醬、青醬。光是這樣你就有 3\*3\*3 = 27 種義大利麵了,但是你心中總是覺得不對勁,這說穿了就只是一些東西在排列組合罷了,有需要為每一種組合都建立一種類別嗎 ? 或許有人會說,為每一種義大利麵建立類別有什麼不好,除了 code 會比較多之外,沒什麼缺點啊 ? 那麼我們來看看義大利麵的價錢吧,通常一道菜的價格都是有跡可循的,而這間義大利麵店也確實如此,好比說你點了一碗"白醬培根斜管麵",那麼這一碗麵的價格其實就是白醬($20)+培根($30)+斜管麵($50)的價錢,因此在"白醬培根斜管麵"的類別之中,價錢便是$100。 ```c++=1 class CreamSauce_Bacon_Penne{ public: int cost(){ return 100; } }; ``` 這個菜單看起來沒毛病,直到某一天培根漲價了 ...,你這時候就得把菜單中所有包含培根的類別都找出來並且更新他們的價錢 ( 小心別加錯了 )。 這時候又有人會說,那麼如果我把類別內的價錢拆分成細項的加總呢 ? 是不是就解決原料價格變動的問題了 ? ```c++=1 int CreamSauce_Prize = 20; int Bacon_Prize = 30; int Penne_Prize = 50; class CreamSauce_Bacon_Penne{ public: int cost(){ return CreamSauce_Prize + Bacon_Prize + Penne_Prize ; } }; ``` 這個問題看起來是解決了沒錯,那麼我們再來看看另一種情況 ... 今天店裡來了一位客人,他也喜歡吃"白醬培根斜管麵",不過他想要客製化他的餐點,因為他實在是太喜歡培根了 ! 因此餐點成了"白醬培根斜管麵(雙倍培根)",可是店裡的菜單顯然沒有這一個類別。 難道你要為了一個客人就去創建一個新的義大利麵類別嗎 ? ```c++=1 int CreamSauce_Prize = 20; int Bacon_Prize = 30; int Penne_Prize = 50; class CreamSauce_DoubleBacon_Penne{ public: int cost(){ return CreamSauce_Prize + 2*Bacon_Prize + Penne_Prize ; } }; ``` > 你的類別越來越多了 ... - 因此 ! 讓我們來使用 Decorator Pattern 吧 ! ```c++=1 class Pasta{ public: virtual int cost() = 0; }; class Decorator : public Pasta{ protected: Pasta* _pasta = nullptr; }; ``` 我們先建立一個抽象的義大利麵類別,並且也建立一個可以修飾義大利麵的裝飾器,特別要注意的是 ,這個裝飾器類別是繼承自義大利麵 ( 雖然有點不太合理 ),並且在裝飾器的內部包含了一個義大利麵的指標 ( 用途之後會揭曉 )。 - 接著我們來建立一個最基本的義大利麵類別吧 ( 只有麵 ) ! ```c++=1 // 圓直麵 class Spaghetti : public Pasta{ public: int cost(){ return 45; } }; // 斜管麵 class Penne : public Pasta{ public: int cost(){ return 50; } }; // 螺旋麵 class Rotini : public Pasta{ public: int cost(){ return 50; } }; ``` 有了基本的麵之後,讓我們建立一些調味料 ( Decorator ) : ```c++=1 class Bacon : public Decorator{ public: Bacon(Pasta* P){ _pasta = P; } int cost(){ // Bacon -> $35 return _pasta->cost() + 30 ; } }; class Chicken : public Decorator{ public: Chicken(Pasta* P){ _pasta = P; } int cost(){ // Chicken -> $35 return _pasta->cost() + 35 ; } }; class TomatoSauce : public Decorator{ public: TomatoSauce(Pasta* P){ _pasta = P; } int cost(){ // Tomato Sauce -> $15 return _pasta->cost() + 15 ; } }; class CreamSauce : public Decorator{ public: CreamSauce(Pasta* P){ _pasta = P; } int cost(){ // Cream Sauce -> $20 return _pasta->cost() + 20 ; } }; ``` 我們在這裡只實作了2種肉品以及2種醬料,不過聰明的你應該可以舉一反三,因為它們都繼承自裝飾器類別,並且提供了修飾 Pasta 類別的能力 ( 在這裡僅修飾了價錢 )。 從 code 中可以看出,裝飾器的能力就是將傳進來的 Pasta 包上一層膜 ( 例如價錢 ),而包膜完之後依然是一個 Pasta ( Decorator 也是 Pasta ! ),所以依然能讓其他的裝飾器繼續修飾 ~ - 成果 : 回到一開始的情境,如果我們要製作一份"白醬培根斜管麵",我們只要這麼做 : ```c++=1 int main(){ Pasta* my_pasta = new Penne(); my_pasta = new Bacon(my_pasta); my_pasta = new CreamSauce(my_pasta); return 0; } ``` 那麼關於前面提到的那位客人所點的"白醬培根斜管麵(雙倍培根)"呢 ? 其實也很簡單 ~ ```c++=1 int main(){ Pasta* my_pasta = new Penne(); my_pasta = new Bacon(my_pasta); my_pasta = new CreamSauce(my_pasta); my_pasta = new Bacon(my_pasta); return 0; } ``` 是的,你僅僅只要將原本的"白醬培根斜管麵"再包覆一層"培根"裝飾器即可 ! 照著個程式架構,那怕是有人想點"紅白雙醬5倍雞排螺旋麵",也絕對難不倒你 ~ > :+1: 設計法則之一 : 類別要歡迎擴展(open),但拒絕修改(close)。 // 2021.10.17 更新 --- ## 4. Factory Pattern 當你創建了一個很複雜的物件 ( 該類別內可能包含很多組件類別 ),因此在這個類別的 constructor 就會有許多的 new ,若是你想要眼不見為淨,或是讓這個類別更有彈性,那麼你或許可以考慮使用這個 Factory Pattern ( 工廠模式 )。 > 工廠模式可以粗分為三種 : 簡單工廠、工廠、抽象工廠。 - 情境 : 繼續接著義大利麵店的故事,你經過幾個月的努力工作之後,你的工作從櫃檯點餐變成了到廚房裡準備餐點。而你又發現了一些問題,就是如何依照顧客的點餐去製作義大利麵呢 ? 我們先假設現在只提供兩種義大利麵,分別是"青醬蛤蜊直麵"以及"白醬培根斜管麵",那麼你會如何設計程式呢 ? 我們先用最直覺的方式寫出你的義大利麵廚房 : ```c++=1 class Pasta{ public: void cook() { std::cout<<"Cook the ingredients into delicious pasta ~\n"; } virtual void show() = 0; }; class CreamSauce_Bacon_Penne : public Pasta{ public: void show(){ std::cout<<"CreamSauce_Bacon_Penne \n"; } }; class PestoSauce_Clams_Spaghetti : public Pasta{ public: void show(){ std::cout<<"PestoSauce_Clams_Spaghetti \n"; } }; class Pasta_Store{ public: Pasta* order(std::string type){ Pasta *pasta = nullptr; if(type=="CreamSauce_Bacon_Penne"){ pasta = new CreamSauce_Bacon_Penne(); } else if(type=="PestoSauce_Clams_Spaghetti"){ pasta = new PestoSauce_Clams_Spaghetti(); } else{ std::cout<<"Error !\n"; return nullptr; } pasta->cook(); return pasta; } }; ``` 這個廚房看起來還算堪用,你確實可以透過 order() 去依照顧客的訂單做出不同的義大利麵,然而當你的品項再次變多時,你會發現你需要去修改你的 order(),而且可以想像的是,最後你的 order() 會充滿一堆 if-else ...,而這些瑣碎的事情明明就跟製作一道義大利麵沒有太大的關係 ! - 因此,我們可以稍微把它修正一下,變成更精簡的樣子 : ```c++=1 class Pasta_Store{ public: Pasta_Store(Simple_Factory F){ _factory = F; } Pasta* order(std::string type){ Pasta *pasta = _factory.createPasta(type); pasta->cook(); return pasta; } private: Simple_Factory* _factory; }; ``` 是的,我們創建了一個獨立的 Simple_Factory 類別,並且將準備食材的事情丟給它來做,而我們只要負責拿到對應的食材,並且把它煮熟就行,而這就是所謂的"簡單工廠模式"。 > 簡單工廠其實不是設計模式,但是它很簡單,也很常被使用。 - 而另一種作法,也就是工廠模式,則是利用了繼承的方式去延後實例化的發生 : ```c++=1 class Pasta_Store{ public: Pasta* order(std::string type){ Pasta *pasta = createPasta(type); pasta->cook(); return pasta; } protected: virtual Pasta* createPasta(std::string type) = 0; }; class Factory : public Pasta_Store{ public: Pasta* createPasta(std::string type){ Pasta *pasta = nullptr; if(type=="CreamSauce_Bacon_Penne"){ pasta = new CreamSauce_Bacon_Penne(); } else if(type=="PestoSauce_Clams_Spaghetti"){ pasta = new PestoSauce_Clams_Spaghetti(); } else{ std::cout<<"Error !\n"; return nullptr; } return pasta; } }; ``` 我們可以看到,Factory 類別繼承自 Pasta_Store,而我們的 Pasta_Store 除了一定要的 order() 之外,還宣告了一個虛擬函式 createPasta(),也就是要求 Factory 去實作它,而在有了 Factory 的幫助之下,order() 中就可以放心的交給 createPasta() 去取得材料,自己只要拿到材料後烹煮即可 ~ 這時候有人一定會提出質疑說,這只是把"髒東西"放到另一個類別而已,你還不是要去維護它 ! 恩 ... 好像是這樣沒錯,但是其實每個類別要作的事更加單純了不是嗎 ? ( Pasta_Store -> 料理程序、Factory -> 提供食材 ) - 我們再來看看另一種模式 : 抽象工廠模式 抽象工廠模式的觀點就和前兩者不一樣了,抽象工廠本身在乎的是如何抽象地生產食材,例如說義大利麵的材料大致上可以分為 "麵、肉、醬",而當你在製作一盤"白醬培根斜管麵"的時候,你叫必須要實例化"斜管麵、培根、白醬",而抽象工廠則是提供一個通用的介面,讓使用者自己決定要使用哪一種工廠去實例化。 ```c++=1 class Abstract_Factory{ public: virtual Meat* createMeat() = 0; virtual Sauce* createSauce() = 0; virtual Noodle* createNoodle() = 0; }; class CreamSauce_Bacon_Penne_Factory : public Abstract_Factory{ public: Meat* createMeat(){ Meat* meat = new Meat("Bacon"); return meat; } Sauce* createSauce(){ Sauce* sauce = new Sauce("Cream"); return sauce; } Noodle* createNoodle(){ Noodle* noodle = new Noodle("Penne"); return noodle; } }; ``` 我們可以看到,抽象工廠只提供了一個介面,而至於會製作出什麼東西,就要看子類別工廠要如何實作了。 因此當你想要取得"白醬培根斜管麵"的材料的時候,你只要照著抽象工廠提供的介面去呼叫,並指定實作的工廠類別是 CreamSauce_Bacon_Penne_Factory 就可以了。 > :+1: 設計法則之一 : 要依賴抽象,不要依賴具體類別。 // 2021.10.17 更新 --- ## 5. Singleton Pattern 身為單身漢,一定就要懂單身漢模式(X) 當你需要建立某種很重要、很特別的類別,而那種類別在你的程式中只能被實例化 ( new ) 一次, 就算你的程式會執行多線程 ( multi-thread ),該類別也不能被建立第二次 ( 否則可能會出大錯 )。 那麼你就可以考慮使用這個 Singleton Pattern ( 單例模式 )。 - 情境 : 沒啥好說的,好比說是掌管機台的類別,或是掌管資料庫的類別,你大概不會希望它長出好幾個 ... 雖然 new 這東西是可以人為避免的,但是如果一開始的類別就設計成只能存在一個,那麼豈不是更令人安心 ? - Singleton 大致上可以粗分為兩種 : Lazy 和 Hungry。 - Lazy Singleton 只會在你第一次 new 到它的類別時,它才會去實例化。 - Hungry Singleton 則是在程式執行的一開始就主動實例化唯一一個物件。 - Lazy Singleton 和 Hungry Singleton 的優缺點 ? - Lazy Singleton 是被動的,所以不會拖累程式一開始的速度,但當你要建立第一次的物件時,此時才會開始實例化,因此就會比較慢。 - Hungry Singleton 則是主動的,所以一開始就會對程式造成負擔,儘管你還沒有要使用它,然而當你要使用它的物件時,就會非常快速,因為早就建立好了。 就讓我們簡單地看一個 Singleton 的架構吧 ~ ```c++=1 class Meyers_Singleton { // a lazy-type singleton, thread-safe public: static Meyers_Singleton& getInstance() { std::cout<<"Create a Meyers Singleton !\n"; static Meyers_Singleton sInstance; return sInstance; } private: Meyers_Singleton() {} }; ``` 我們會發現,Singleton 的特色就是將它的 constructor 放在 private ! 也就是說,沒有人可以在程式區直接建立它,而間接建立物件的方式,則由 Singleton 自己去提供 ( 球員兼裁判 )。 而這個 Meyers 所設計的 Singleton 方法則是利用了 static 的特性,讓它從自己的肚子裡出生,並且只會出生一次。 // 2021.10.17 更新 --- ## 6. Command Pattern 當你在程式中需要運用到類似於命令的功能,而這些命令的數量之多,類型也五花八門,那麼你就該搬出命令模式 ( Command Pattern ) 了 ! - 情境 : 就以遙控器為例子吧 ~ 假設你拿到了一個神奇的遙控器,它可以控制你家裡所有的家電,不過由於遙控器的按鈕有限,所以說你一次只能在遙控器上擁有一種家電的控制能力,否則就要再設定一次,改變控制的對象。 我們先來看家電的程式以及一個糟糕的遙控器程式 : ```c++=1 class Door{ public: Door(std::string str){ _description = str; } void open(){ std::cout<<"Open "<<_description<<std::endl; } void close(){ std::cout<<"Close "<<_description<<std::endl; } private: std::string _description; }; class TV{ public: TV(std::string str){ _description = str; } void on(){ std::cout<<"Turn on "<<_description<<std::endl; } void off(){ std::cout<<"Turn off "<<_description<<std::endl; } private: std::string _description; }; class Remote_Control{ public: void press_button_1(){ _device->on(); } void press_button_2(){ _device->off(); } private: TV* _device = new TV("My TV"); }; ``` 這個遙控器看起來確實可以控制電視的開關,但是如果你此時想要讓遙控器換成控制門的開關呢 ? 看起來你也只能把遙控器的程式重新寫過 ... - 然後這是萬能遙控器第二代 : ```c++=1 class Remote_Control_2{ public: void set_device(std::string str){ _device = str; } void press_button_1(){ if(_device=="TV"){ _tv->on(); } else if(_device=="door"){ _door->open(); } } void press_button_2(){ if(_device=="TV"){ _tv->off(); } else if(_device=="door"){ _door->close(); } } private: std::string _device; TV* _tv = new TV("My TV"); Door* _door = new Door("My Door"); }; ``` Hummm ... 現在你的遙控器確實聰明一點了,它可以透過 set_device() 去決定你接下來想要控制哪一個物品,然而我們會發現,這樣子的作法讓遙控器的程式中充滿了 if-else,而且每當你的家電時有所變動時,都必須要來修改你的遙控器,有沒有更加高端的寫法呢 ? - 讓我們使用命令模式來撰寫萬能遙控器第三代吧 ! 不過在設計遙控器之前,我們要先把"命令"類別化,而這也正是命令模式的精隨之處 ~ ```c++=1 class Command{ public: virtual void execute_1() = 0; virtual void execute_2() = 0; }; class Door_Command : public Command{ public: void execute_1(){ _door->open(); } void execute_2(){ _door->close(); } private: Door* _door = new Door("My Door"); }; class TV_Command : public Command{ public: void execute_1(){ _tv->on(); } void execute_2(){ _tv->off(); } private: TV* _tv = new TV("My TV"); }; ``` 我們建立了一個抽象的命令類別,並宣告了兩個抽象方法,而控制門的方法也變成一個命令類別,並實作先前宣告的兩個方法,控制TV的方法也是如此。 - 再來看看遙控器本身 : ```c++=1 class Remote_Control_3{ public: void set_device(Command* command){ _command = command; } void press_button_1(){ _command->execute_1(); } void press_button_2(){ _command->execute_2(); } private: Command* _command = nullptr; }; ``` 是不是變得乾淨簡潔多了 ! 透過上面實作的命令類別,遙控器只要根據抽象的命令介面去執行命令,就可以達到預期的效果,而不需要知道底層到底是怎麼實作的。 > :+1: 設計法則之一 : 把會變的東西封裝起來,留下不會變的。 // 2021.10.18 更新 --- ## 7. Adapter Pattern 當你有兩種不同的類別,而這兩個類別雖然有著相似的介面,但是還是無法直接串接使用 ( 當 type-C遇見 iphone ),此時你就可以使用轉接器模式 ( Adapter Pattern ) ! - 情境 : 讓我們把鴨子類別再拿出來看看吧,不過為了簡單起見,我們先把策略模式放一旁 XD ```c++=1 class Duck{ public: virtual void quack() = 0; virtual void fly() = 0; }; ``` 我們建立了一個抽象的鴨子類別,並且定義了兩個抽象方法,代表著所有的鴨子都必須實做叫跟飛。 因此我們就可以依樣畫葫蘆建立一個實體的鴨子類別 : ```c++=1 class Redhead_duck : public Duck{ public: void quack(){ std::cout<<"Quack !\n"; } void fly(){ std::cout<<"~ ~ ~ ~ ~>\n"; } }; ``` 然後我們可以簡單的測試一下鴨子的功能 : ```c++=1 int main(){ Duck *my_duck = new Redhead_duck(); my_duck->quack(); my_duck->fly(); return 0; } ``` 看起來我們的鴨子很健康,那麼說好的問題呢 ? - 因此我請朋友帶來了一隻烏骨雞,而它當然是屬於雞的類別,我們來看一下定義 : ```c++=1 class Chicken{ public: virtual void cluck() = 0; virtual void jump() = 0; }; // 烏骨雞 class Silkie : public Chicken{ public: void cluck(){ std::cout<<"Cluck !\n"; } void jump(){ std::cout<<"~>\n"; } }; ``` 根據定義,所有的雞都會咕咕叫以及跳躍,而烏骨雞則實作了這兩個動作。 但是,我們只養過鴨子,因此我們只知道 quack()、fly() 這兩個命令,顯然烏骨雞並不吃這一套 ... 一隻不受控制的雞在家裡亂竄可是非常的不妙,因此我們就得趕緊使用 " Adapter Pattern " ! - 由於我們只會控制鴨子,因此我們必須把烏骨雞包上一層鴨皮 : ```c++=1 class Chicken_Adapter : public Duck{ public: Chicken_Adapter(Chicken* chicken){ _chicken = chicken; } void quack(){ _chicken->cluck(); } void fly(){ for(int i=0;i<5;i++) _chicken->jump(); } private: Chicken* _chicken; }; ``` 我們可以看到這個"雞轉鴨"轉換器當中包含了一隻雞,並且繼承自鴨子類別,因此這個轉換器就有著和其他鴨子一樣的基本功能,但是這個轉換器其實並不是真的鴨子,所以當它被命令去執行動作時,就可以用雞來頂替,而至於要使用雞的那些功能才會"合理",就交給你來決定了 ~ 因為 quack() 和 cluck() 都是叫聲,所以這邊就把它們串接起來,而 fly() 和 jump() 都是移動方式,所以也是可以拿來頂替的,不過由於 jump() 通常移動距離都比 fly() 來的近,因此我這邊讓它多跳了幾次,看起來功能才會比較像 fly()。 - 成果 : 現在我們可以使用這個轉接器來命令烏骨雞了 ! ```c++=1 int main(){ Chicken *my_chicken = new Silkie(); Duck *my_duck = new Chicken_Adapter(my_chicken); my_duck->quack(); // my_chicken->cluck() my_duck->fly(); // my_chicken->jump() * 5 return 0; } ``` // 2021.10.18 更新 --- ## 8. Facade Pattern 假如你現在手上有一堆類別以及方法,而假設你有一個常常會被使用到的功能,它需要使用眾多的類別、經過繁複的步驟才能完成,那麼你其實可以創建一個門面類別,幫你 " 一鍵完成 "。 - 情境 : 我們再一次的回到義大利麵店,你依然在廚房擔任廚師,然而雖然你熟知烹煮義大利麵以及其他品項的流程,但是你還是覺得步驟太多了 ! 讓我們看一下廚房的狀況 : ```c++=1 class Fridge{ public: void take(std::string str){ std::cout<<"<fridge> Take out "<<str<<std::endl; } }; class Pot{ public: void put(std::string str){ std::cout<<"<pot> Put in "<<str<<std::endl; } void take(std::string str){ std::cout<<"<pot> Take out "<<str<<std::endl; } void clear(){ std::cout<<"<pot> Clear the pot\n"; } void heat(int time){ std::cout<<"<pot> Heat for "<<time<<" minutes\n"; } }; class Dish{ public: void put(std::string str){ std::cout<<"<dish> Put in "<<str<<std::endl; } void mix(){ std::cout<<"<dish> Mix the ingredients\n"; } }; ``` 上面這些功能連小朋友都懂,但是讓我們實際來煮一盤 "白醬培根斜管麵" 吧 ! ```c++=1 int main(){ Pot pot; Dish dish; Fridge fridge; fridge.take("CreamSauce"); fridge.take("Bacon"); fridge.take("Penne"); pot.put("Water"); pot.heat(5); pot.put("Penne"); pot.heat(3); pot.take("Penne"); pot.clear(); dish.put("Penne"); dish.put("Bacon"); dish.put("CreamSauce"); dish.mix(); return 0; } ``` 因為篇幅考量,我們這個烹煮程序已經省略許多細節了,但是依然看起來非常麻煩 ! 把這麼多的步驟直接攤在陽光下,不僅看起來很醜,別人也不知道這是在幹嘛。 - 所以說我們來運用門面模式來做一個 "一鍵烹煮" 吧 : ```c++=1 class Facade{ public: void Make_CreamSauce_Bacon_Penne(){ _fridge.take("CreamSauce"); _fridge.take("Bacon"); _fridge.take("Penne"); _pot.put("Water"); _pot.heat(5); _pot.put("Penne"); _pot.heat(3); _pot.take("Penne"); _pot.clear(); _dish.put("Penne"); _dish.put("Bacon"); _dish.put("CreamSauce"); _dish.mix(); } void Make_Salad(){ _fridge.take("Fruits and Vegetables"); _fridge.take("Bacon"); _fridge.take("Dressing"); _dish.put("Fruits and Vegetables"); _dish.put("Bacon"); _dish.put("Dressing"); _dish.mix(); } private: Pot _pot; Dish _dish; Fridge _fridge; }; ``` 我們把這些繁瑣的步驟封裝進門面類別了 ( 順便加了一個沙拉的作法 ) ! 這樣我們下次需要製作這些料理時,就可以使用這個門面一鍵完成 ~ ```c++=1 int main(){ Facade order; order.Make_CreamSauce_Bacon_Penne(); std::cout<<"-------------------\n"; order.Make_Salad(); return 0; } ``` // 2021.10.18 更新 --- ## 9. Template Pattern 當你有一些相似的類別,而你想要為它們設計一套通用的演算法,那麼你就可以利用樣板模式 " Template Pattern " 來幫你寫出一個半抽象的演算法流程。當不同的實體類別去使用它時,就會產生 "因類別而異" 但是大致相同的效果。 - 情境 : Coffee or Tea ? 這句話想必不陌生,因此我們今天就用咖啡和茶來舉例。 咖啡和茶顯然是兩種不同的飲品,但是他們其實也有相似之處對吧 ? 我們來看看咖啡和茶各自的沖泡流程 : - 茶 : 拿杯子 -> 加熱水 -> 放入茶包 -> 完成 - 咖啡 : 拿杯子 -> 加熱水 -> 加咖啡粉 -> 加糖 -> 完成 我們可以看出,沖泡的細節雖然有不同,但是整體上好像又差不多 ( 拿杯子 -> 加熱水 -> 放入東西 ) 因此我們就可以利用樣板模式,設計出一個沖泡飲品的大方向,再讓茶和咖啡規劃自己的特殊細節。 - 以下是我從其他網站抓下來修改的,雖然沒有把它直接用真實例子去命名 ( 我懶XD ),不過還是可以靠自己把茶和咖啡的影子對應到程式之中 ~ ```c++=1 class Abstract_Class { public: void Your_Algorithm() const { this->Base_Operation_1(); this->Base_Operation_2(); this->Required_Operation_1(); this->Required_Operation_2(); this->Hook_1(); this->Hook_2(); } protected: // 這裡是抽象類別提供的實體方法,所以子類別不用實作,直接用就好 void Base_Operation_1() const { std::cout << "<Abstract Class> : Base Operation 1\n"; } void Base_Operation_2() const { std::cout << "<Abstract Class> : Base Operation 2\n"; } // 這裡是抽象類別宣告的純抽象方法,子類別必須自己定義實作 virtual void Required_Operation_1() const = 0; virtual void Required_Operation_2() const = 0; // 這裡是抽象類別宣告的半抽象方法(Hook),子類別可以自己決定要不要實作 virtual void Hook_1() const {} virtual void Hook_2() const {} }; class Concrete_Class_1 : public Abstract_Class { protected: void Required_Operation_1() const override { std::cout << "<Concrete Class 1> : Required Operation 1\n"; } void Required_Operation_2() const override { std::cout << "<Concrete Class 1> : Required Operation 2\n"; } }; class Concrete_Class_2 : public Abstract_Class { protected: void Required_Operation_1() const override { std::cout << "<Concrete Class 2> : Required Operation 1\n"; } void Required_Operation_2() const override { std::cout << "<Concrete Class 2> : Required Operation 2\n"; } // 此類別需要用到特殊功能,所以實作了 Hook_1() void Hook_1() const override { std::cout << "<Concrete Class 2> : Hook 1\n"; } }; void Client_Code(Abstract_Class *class_) { // ... class_->Your_Algorithm(); // ... } ``` // 2021.10.19 更新 --- ## 10. Iterator Pattern 在聽過設計模式之前,你大概就已經先知道 Iterator 了,原因是 C++ 的 STL Container 基本上都有 Iterator 的身影,而 Iterator 的好處就是你不用真的知道 Container 的底層是如何實作的,你也能去對 Container 的成員進行操作,而且當你要對這個 Container 設計一套演算法時,如果善用 Iterator 進行操作,你根本就不需要去動到 Container 原本的程式,非常符合 Open-Close 的原則。 - 基本上就是讓你的類別多出一個提供 Iterator 的管道,並且定義幾個常見的 Iterator 操作。 --- ## 11. Composite Pattern 點開你的檔案總管,裡面充滿了資料夾以及檔案,而資料夾裡面又可以包含資料夾和檔案,因此就會產生一個樹狀的結構,並且你對於資料夾和檔案都可以進行相同的操作 ( 點擊、命名、複製 ... )。 這就是一個組合模式 " Composite Pattern " 的例子。 --- ## 12. State Pattern Finite State Machine 算是一種蠻常見的設計,因此相對應的設計模式就出現了。 狀態模式 ( State Pattern ) 可以讓你設計出比較好維護的 Finite State Machine ! - 情境 : 讓我們來看點正經的,底下那張圖就是我們準備要寫成程式的狀態圖, 而它的功用則是偵測輸入的訊號何時從 0 上升到 1, 當訊號上升時,輸出訊號就換短暫的出現 1,其餘時間都是 0。  - 那麼我們先來定義一個 Rising Edge Detector 的雛型吧 : ```c++=1 class Rising_Edge_Detector{ public: void set_state(int s); void set_input_zero(); void set_input_one(); }; ``` 我們可以從上面的狀態圖可以知道,狀態只有2種,而輸入也只有兩種(0、1),因此這邊就宣告了 set_input_zero() 和 set_input_one(),分別代表著輸入是 0 和 1 時會做出的反應。 - 狀態 : 狀態模式的核心就是把狀態類別化 ( 而不是用一堆 if-else ), 當然,為了能夠在不同的狀態之間轉換,我們需要建立一個抽象的狀態類別 : ```c++=1 class State{ public: virtual void zero() = 0; virtual void one() = 0; protected: Rising_Edge_Detector* _detector; }; ``` 這個抽象類別宣告了兩個 virtual function,分別代表著當前狀態遇到輸入是 0、1 的行為。 此外,這個類別還包含著一個 Rising_Edge_Detector 的指標,用途稍後就會揭曉 ~ 我們先接著看看如何實作兩個實體類別 : ```c++=1 class State_Zero : public State{ public: State_Zero(Rising_Edge_Detector* D){ _detector = D; } void zero(){ std::cout<<"input = 0 , output : 0 \n"; } void one(){ _detector->set_state(1); std::cout<<"input = 1 , output : 1 \n"; } }; class State_One : public State{ public: State_One(Rising_Edge_Detector* D){ _detector = D; } void zero(){ _detector->set_state(0); std::cout<<"input = 0 , output : 0 \n"; } void one(){ std::cout<<"input = 1 , output : 0 \n"; } }; ``` 每個實體狀態類別中都必須要實作 zero() 以及 one(),因此只要輸入是正確的,就不會發生未定義的情況。在 State_Zero 中的 one(),重新設定了 Rising_Edge_Detector 的狀態,State_One 中的 zero() 也是如此,因此我們才需要在 State 中包含一個 Rising_Edge_Detector ( 為了能將結果回饋回去 )。 - 回到 Rising_Edge_Detector,我們接著可以把尚未寫好的 function 完成了 : ```c++=1 class Rising_Edge_Detector{ public: Rising_Edge_Detector(){ _zero = new State_Zero(this); _one = new State_One(this); _state = _zero; } void set_state(int s){ if(s==0) _state = _zero; else if(s==1) _state = _one; } void set_input_zero(){ _state->zero(); } void set_input_one(){ _state->one(); } private: State* _state; State_Zero* _zero; State_One* _one; }; ``` 我們可以發現,Rising_Edge_Detector 當中也包含了 State 的指標,並且也建立了兩個實體的狀態,而這些實體狀態當然會指向 Detector 自己 ( 讓它們有能力改變 Detector 的狀態 ),而巧妙的是,在 set_input_zero()、set_input_one() 當中,並不需要 if-else 判斷,因為只是單純的把訊號輸入委託給狀態類別去處理。 // 2021.10.19 更新 ---
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up