Design Pattern(1) - Strategy
April 07, 2017以下文章是閱讀 深入淺出Design Pattern 還有 聖經還有Source making的筆記 圖片截圖自lynda.com的Foundations of Programming: Design Patterns 要更深入的理解一定要去看這兩本書
Inheritance
對OO有sense的人都知道 Inheritance是個很好reuse code的方式 比如說紅頭鴨跟綠頭鴨 你不需要寫兩個一樣的swim function 你應該寫一個Duck的swim function 然後讓這兩隻鴨子繼承他們
一圖勝過千言萬語 這裡Duck的Display是abstract method 每個繼承Duck的class都要implement display
嗯嗯非常好 那今天如果來了個橡皮黃色小鴨 雖然這橡皮鴨不會叫 但沒關係 我們overwrite Duck的quack就搞定惹
Design Pattern rule #0: 軟體開發的世界裡 唯一不變的真理就是需求一直在變
今天你老闆跟你說 唉我突然想到鴨子好像都會飛 我們必須要有fly這個function
為了要reuse fly這個function 我們把它加進Duck裡 這樣所有會飛的鴨子都不用再重複寫 但對於所有不會飛的鴨子 我們就把fly overwrite掉
但這樣其實又有其他的問題 那不會飛的鴨子的fly function其實也是duplicate code 那如果我最後不會飛的鴨子的class比會飛的多 那Duck裡面是不是要把fly改成nofly 讓那些會飛的去overwrite nofly?
更重要的問題 inheritance的設計會讓所有鴨子的行為在compile time就被寫死 我今天設計的遊戲裡橡皮鴨吃到無敵星星想飛起來 抱歉辦不到
Interface
以前好像學過interface 讓那些會飛的鴨去implement飛的interface 會叫的鴨去implement叫的interface不就好了?
這裡實線是繼承虛線是implement
DecoyDuck 不會飛也不會叫 就都不implement 要是明天有新的behavior比如說走路 那就所有會走路的去implement走路的interface即可 這個idea如何
這個設計code reuse完全不行 所有會fly的鴨都要寫一次fly method(即使對於所有會飛的鴨做的事都一樣) 而且要是今天fly的method有改 你必須改掉所有有fly method的class
compile time寫死的問題也沒有解決
把問題歸零
Design Pattern rule #1: Encapsulate what varies
把所有可能會更動的地方獨立出來 不要跟不會變動的地方混在一起
看一下哪裡是會更動的: fly和quack和display 可是display基本上無可避免必須每個class都implement 所以也沒有什麼code reuse的問題
看一下哪裡是不會更動的: swim
Design Pattern rule #2: Program to an interface, not an implementation
之前的設計的主要問題是我們執著於要在Duck或是Duck的subclass裡面implement這兩個function(fly, quack)
現在我們把這兩個動作分離出來
QuackBehavior和FlyBehavior是兩個interface 各自有各自的subclass去implement interface裡面唯一的function
這下high了 flyBehavior和quackBehavior變成Duck的兩個instance
這樣的設計 我還可以runtime setFlyBehavior 如果我突然想讓橡皮鴨飛也沒問題
原本的fly變成performFly 因為現在這件事不是我負責了
我在performFly裡去叫flyBehavior.fly() 我compile time也不知道我到底是怎麼飛的 這是那個implement FlyBehavior的那個subclass(FlyWithWings或是FlyNoWay)負責的
每個繼承Duck的subclass只需要implement display
來個鴨子吧
對於每一個鴨子的subclass 我只要在它的constructor裡new好每一個behavior的instance 一切搞定
因為我Duck裡還有SetFlyBehavior 所以我還可以runtime改變鴨子的飛行方式
如果你當初把behavior的實踐寫死在Duck裡 現在就無法動態改變behavior 這也是DP rule#2厲害的地方
以後你有新的飛行方式 去implement FlyBehavior這個interface就好了 不需要改變其他client對於你的用法
天阿這也太厲害了吧
Strategy pattern
定義演算法家族 並把每個演算法封裝起來 演算法之間彼此可以互換
這個模式讓演算法的變動 不影響演算法的使用方式
把抽象的方法抽離在interface 把實作留給interface的subclass
連用三行的粗體字實在是違背這個部落格化繁為簡的風格 但我認為每一句話都重要到不得不強調
這裡的演算法指的就是fly跟quack
來看一下Strategy pattern的結構
- Context(Duck): 用ConcreteStrategy的物件 來設定自己的狀態
- Strategy(FlyBehavior): 制定所有演算法的共同介面 Context透過這個interface呼叫ConcreteStrategy實作的演算法
- ConcreteStrategy(FlyWithWings): 根據Strategy的interface實作演算法
Strategy pattern也可以說是對演算法的封裝 把演算法的責任和演算法實作本身交給不同人處理
優點
-
Strategy可以針對同一種行為有不同的實作
-
Strategy interface還可以玩自己的hierarchy遊戲 繼承的機制可以把共有的功能提出來
-
對於Context來說 每個subclass有他自己的演算法(可能和其他人重複 可能不重複) 如果把演算法實作寫在Context裡會很難maintain
缺點
-
Context必須對Strategy的實作清楚 才知道要挑哪一種演算法來用
-
Strategy和Cotext的通訊負擔變重 因為不論ConcreteStrategy裡實作起來簡單還是複雜 都是共用Strategy介面 Strategy介面的某些東西 可能一個簡單的ConcreteStrategy根本不需要 或是Context給它的初始化參數它用不到 這種情況也許你不該用Strategy pattern
怎麼Apply
-
定義所有的可以互相交換的演算法的共同interface
-
把演算法的實作細節留給derived class
-
客戶端只跟Interface綁死 不要跟實作綁死(minimize coupling)
DP rule#3 多用合成(Has-a)少用繼承(Is-a)
從這篇文章的例子你看得出來用inheritance之後很多事情都被侷限住了 但用Composition的話我們的設計就多了許多的彈性
鴨子的行為不該是繼承而來 而是和其他的行為物件合成而來
大方向就是不會變的用繼承 會變的用合成