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」,當年我真是完全誤解了 strategy pattern 和 template method pattern 在設計上的初衷。
先舉一個跟 design pattern 完全沒有關係 的例子,但我在 2001 年卻曾經把這個例子誤會成 design pattern。
以下這個例子和 design pattern 沒有任何關係
#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 的實作碼而己。
把 Liskov Substitution Principle 這個概念導入會不會就是 strategy pattern?
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 的時候,會不會知道每一個哺乳動物是怎麼跑的?甚至是不是可以假設每一個哺乳動物都會跑?
/*
* 哺乳動物
*/
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
的本意是要解決什麼類型的問題:
- 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
#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;
}
我們看一下這一段
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 的時候選擇執行不一樣的實作碼,雖然語法正確,但是違反了物件導向設計的 SOLID 五大基本原則之一的 OCP (Open–Closed Principle)。怎麼說呢?假如今天新增了一個類別 Cow,那 run
這個 function 就要再進行修改,要改成這樣,增加第 6, 7
行。
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]
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 的實作碼錯在什麼地方
/*
* 哺乳動物
*/
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 的時候會執行什麼。
最後,我們把一開始的範例做一個修改
#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;
}
$ ./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,還是只用到繼承而己。
我們看以下這個範例
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
dog->run();
cat->run();
B
Test *test = new Test();
test->run(dog);
test->run(cat);
design patterns
strategy pattern