# 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`