# Design patterns 筆記 - Strategy pattern on C++ contributed by < `jeffrey.w` > 這篇筆記回顧我 2001 年剛學 design patterns 的時候對 strategy pattern 的誤解,透過**重新審視 2001 那年的誤解**,重新學習 strategy pattern。 Strategy pattern 很容易跟 template method pattern 混淆,2001 年我剛學這兩種 pattern 的時候,甚至直接**誤以為**「把實作碼寫在 base class 讓 derived class 繼承就是 design pattern」,當年我真是<font color='red'>完全誤解</font>了 strategy pattern 和 template method pattern 在設計上的初衷。 先舉一個跟 design pattern **完全沒有關係** 的例子,但我在 2001 年卻**曾經**把這個例子**誤會**成 design pattern。 <font color='red'>以下這個例子和 design pattern 沒有任何關係</font> ```cpp #include <iostream> /* * 哺乳動物 */ class Mammalia { public: void run(){ std::cout << "run with four legs" << std::endl; } }; class Dog : public Mammalia { }; class Cat : public Mammalia { }; /* * gcc wrong.cpp -lstdc++ -o wrong */ int main() { Dog dog; Cat cat; dog.run(); cat.run(); return 0; } ``` 上面這個例子,跟 design pattern **沒有任何關係**,僅僅只不過是讓 Dog 和 Cat 這兩個類別不用重複打 run 的實作碼而己。 把 **L**iskov **S**ubstitution **P**rinciple 這個概念導入會不會就是 strategy pattern? ```cpp int main() { Mammalia *mammalia = new Dog(); mammalia->run(); delete(mammalia); mammalia = new Cat(); mammalia->run(); delete(mammalia); return 0; } ``` 看起來真的很像 `strategy pattern`,如果沒有再仔細看 Mammalia 的 implementation,真的就會**誤以為**這是 strategy pattern 了。 2001 年的時候我真的**誤以為**這樣就是 strategy pattern,甚至也把這個當成 template method pattern,當時我完全沒有意識到我己經**從根本上就誤會**了 `strategy pattern` 和 template method pattern 在設計上的初衷。接著我將**重新來審視一下我錯在什麼地方**。 先問自己一個問題,設計 Mammalia 這個 class 的時候,會不會知道每一個哺乳動物是怎麼跑的?**甚至是不是可以假設每一個哺乳動物都會跑**? ```cpp /* * 哺乳動物 */ class Mammalia { public: void run(){ std::cout << "run with four legs" << std::endl; } }; ``` 顯然,設計 Mammalia 這個 class 的時候並**沒有辦法得知每一個哺乳動物都是怎麼跑的**,所以**直接在 class Mammalia 實作 run 這個 function 是不合適的**。雖然對於用**兩隻腿**跑的哺乳動物可以用 override,但對於鯨魚,由於鯨魚不會跑,所以也是要用 override。 於是,base class 的實作是 "run with four legs",某些哺乳類另外 override 成 "run with two legs",某些哺乳類另外 override 成 "no legs to run"。 **這意味著每個繼承 Mammalia 的 class 都要去思考 run() 要不要 override**,而思考要不要 override 的時候,就要想一下 Mammalia class 所實作的 run() 符不符合 derived class 的 run() 的需求。 這也 **意味著每個繼承 Mammalia class 的 derived class 要確保正確執行就要看一下 Mammalia class 的 run 的實作碼**。 於是,問題來了,這樣設計繼承體系到底有什麼意義?原本你不需要 run() 的時候你只需要不實作 run() 就可以了,現如今不管你需不需要 run() 你都需要去看 Mammalia class 的 run 的實作碼,更糟糕的是,**需要去看 Mammalia class 的 run 的實作碼** 只是第二步,不是第一步。為什麼只是第二步而不是第一步?因為第一步是 **你要先知道你需要去看 Mammalia class 的 run 的實作碼**。 至此,再重新問自己一個問題,這樣設計繼承體系到底有什麼意義? **沒有意義!一點意義都沒有!** 這樣設計繼承體系是沒有意義的!這只是為繼承而繼承,以後在維護的時候還要去評估哪些 function 必須要 override、哪些 class 需要 override 哪些 function。**而且在評估這些東西之前,你還得先知道有哪些東西需要評估。** 這也不是 strategy pattern,這只是增加無謂的複雜性、無謂的增加維護的痛苦和困難度而己。 很遺憾的是,在 2001 年的時候,我並沒有意識到我對 strategy pattern 的誤解,我也完全沒有意識到我對於繼承和 override 的誤用會對日後進行維護工作的工程師造成多大的痛苦。 不過慶幸的是,當時我還只是一個沒有工作的學生,所以我並沒有把這種東西上線,頂多只是讓錯誤的知識跟著我好幾年而己。因此,這次透過重新審視過去的觀念,讓未來可以避免犯下害人害己的錯誤。 **要不要 override** 並不是 **「這不是 strategy pattern」的主要原因**,所以,我們先看一下 `strategy pattern` 的本意是要解決什麼類型的問題: [[reference, w3sdesign](http://w3sdesign.com/?gr=b09&ugr=proble)] > - How can a class be configured with an algorithm at run-time instead of implementing an algorithm directly? > - How can an algorithm be selected and exchanged at run-time? 把一開始的例子重寫,看看怎麼在 run-time 的時候,選擇執行不一樣的 **run** ```cpp #include <iostream> /* * 哺乳動物 */ class Mammalia { public: virtual ~Mammalia() = default; }; class Dog : public Mammalia { }; class Cat : public Mammalia { }; void run(Mammalia *mammalia){ if(dynamic_cast<Dog *>(mammalia) != nullptr) { std::cout << "run with two legs with dog-style" << std::endl; } else if(dynamic_cast<Cat *>(mammalia) != nullptr) { std::cout << "run with two legs with cat-style" << std::endl; } } /* * gcc wrong-2.cpp -lstdc++ -o wrong-2 */ int main() { Dog dog; Cat cat; run(&dog); run(&cat); return 0; } ``` 我們看一下這一段 ```cpp void run(Mammalia *mammalia){ if(dynamic_cast<Dog *>(mammalia) != nullptr) { std::cout << "run with two legs with dog-style" << std::endl; } else if(dynamic_cast<Cat *>(mammalia) != nullptr) { std::cout << "run with two legs with cat-style" << std::endl; } } ``` 這段允許在 run-time 的時候選擇執行不一樣的實作碼,雖然語法正確,但是違反了物件導向設計的 S**O**LID 五大基本原則之一的 OCP (**O**pen–**C**losed **P**rinciple)。怎麼說呢?假如今天新增了一個類別 Cow,那 `run` 這個 function 就要再進行修改,要改成這樣,增加第 `6, 7` 行。 ```cpp= void run(Mammalia *mammalia){ if(dynamic_cast<Dog *>(mammalia) != nullptr) { std::cout << "run with two legs with dog-style" << std::endl; } else if(dynamic_cast<Cat *>(mammalia) != nullptr) { std::cout << "run with two legs with cat-style" << std::endl; } else if(dynamic_cast<Cow *>(mammalia) != nullptr) { std::cout << "run with two legs with cow-style" << std::endl; } } ``` 也就是說,每增加一個類別,**`run` 就要修改一次**,而 Open–Closed Principle 這個原則講的是 `should be open for extension, but closed for modification` [[reference, wiki](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle)] > software entities (classes, modules, functions, etc.) should be **open** for extension, but **closed** for modification 如果這種 if-else 只有四五個,其實還算好維護,然而,**當 if-else 成長到十幾個、幾十個的時候**,後續的維護人員真的可以單純只要加一個 if-else 而不用去看其他的 if-else 嗎? 要如何做到每次在擴充的時候**不用去改**原本己經寫好的程式?在這個範例裡,我們可以使用 `strategy pattern`。 回到一開始的範例,我們看一下 Mammalia 的實作碼錯在什麼地方 ```cpp /* * 哺乳動物 */ class Mammalia { public: void run(){ std::cout << "run with four legs" << std::endl; } }; ``` 我們希望能在 run-time 的時候選擇執行不一樣的實作碼,但 Mammalia 身為 base class,直接就把實作碼寫在 base class 。這裡的問題**不在於 derived class 要不要 override**,而是在於**這個情境的 base class 不應該假設知道 run-time 的時候會執行什麼**。 最後,我們把一開始的範例做一個修改 ```cpp #include <iostream> /* * 哺乳動物 */ class Mammalia { public: virtual void run() = 0; // pure virtual function }; class Dog : public Mammalia { public: void run() override { std::cout << "run with two legs with dog-style" << std::endl; } }; class Cat : public Mammalia { public: void run() override { std::cout << "run with two legs with cat-style" << std::endl; } }; /* * gcc correct.cpp -lstdc++ -o correct */ int main() { Mammalia *dog = new Dog(); Mammalia *cat = new Cat(); dog->run(); cat->run(); return 0; } ``` ```shell $ ./correct run with two legs with dog-style run with two legs with cat-style ``` 在這個版本,如果新增一個 `class Cow`,**不用改到**原本己經寫好的程式。不過,每一個 derived from Mammalia 的 class 都**必須**要 override `run` 這個 function,因為 `run` 是 `pure virtual function`。 But,這裡仍舊沒有用到 strategy pattern,感謝 @rayshih 的提醒,這個範例換掉的是整個 object,還是只用到繼承而己。 我們看以下這個範例 ```cpp= class Test { public: void run(Mammalia *animal){ animal->run(); } }; /* * gcc correct.cpp -lstdc++ -o correct */ int main() { Dog *dog = new Dog(); Cat *cat = new Cat(); Test *test = new Test(); test->run(dog); test->run(cat); return 0; } ``` 比較這兩個範例的差別,**A** 是以整個 object 為單位做替換,**B** 的替換卻是為了 **使用不同的 run 的實作**。 **A** ```cpp= dog->run(); cat->run(); ``` **B** ```cpp= Test *test = new Test(); test->run(dog); test->run(cat); ``` ## References - [ ] [Design Patterns: Elements of Reusable Object Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides](https://pdfs.semanticscholar.org/a5a7/17572be243e6ad1f874ba3567eb22c4026f9.pdf) - [ ] [The Strategy design pattern - Problem, Solution, and Applicability. w3sDesign.com. Retrieved 2017-08-12.](http://w3sdesign.com/?gr=b09&ugr=proble) ###### tags: `design patterns` `strategy pattern`