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 沒有任何關係
上面這個例子,跟 design pattern 沒有任何關係,僅僅只不過是讓 Dog 和 Cat 這兩個類別不用重複打 run 的實作碼而己。
把 Liskov Substitution Principle 這個概念導入會不會就是 strategy pattern?
看起來真的很像 strategy pattern
,如果沒有再仔細看 Mammalia 的 implementation,真的就會誤以為這是 strategy pattern 了。
2001 年的時候我真的誤以為這樣就是 strategy pattern,甚至也把這個當成 template method pattern,當時我完全沒有意識到我己經從根本上就誤會了 strategy pattern
和 template method pattern 在設計上的初衷。接著我將重新來審視一下我錯在什麼地方。
先問自己一個問題,設計 Mammalia 這個 class 的時候,會不會知道每一個哺乳動物是怎麼跑的?甚至是不是可以假設每一個哺乳動物都會跑?
顯然,設計 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
我們看一下這一段
這段允許在 run-time 的時候選擇執行不一樣的實作碼,雖然語法正確,但是違反了物件導向設計的 SOLID 五大基本原則之一的 OCP (Open–Closed Principle)。怎麼說呢?假如今天新增了一個類別 Cow,那 run
這個 function 就要再進行修改,要改成這樣,增加第 6, 7
行。
也就是說,每增加一個類別,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 的實作碼錯在什麼地方
我們希望能在 run-time 的時候選擇執行不一樣的實作碼,但 Mammalia 身為 base class,直接就把實作碼寫在 base class 。這裡的問題不在於 derived class 要不要 override,而是在於這個情境的 base class 不應該假設知道 run-time 的時候會執行什麼。
最後,我們把一開始的範例做一個修改
在這個版本,如果新增一個 class Cow
,不用改到原本己經寫好的程式。不過,每一個 derived from Mammalia 的 class 都必須要 override run
這個 function,因為 run
是 pure virtual function
。
But,這裡仍舊沒有用到 strategy pattern,感謝 @rayshih 的提醒,這個範例換掉的是整個 object,還是只用到繼承而己。
我們看以下這個範例
比較這兩個範例的差別,A 是以整個 object 為單位做替換,B 的替換卻是為了 使用不同的 run 的實作。
A
B
design patterns
strategy pattern