什么是策略模式?
??策略模式作為一種軟件設計模式,指對象有某個行為,但是在不同的場景中,該行為有不同的實現(xiàn)算法。比如每個人都要“交個人所得稅”,但是“在美國交個人所得稅”和“在中國交個人所得稅”就有不同的算稅方法。
- 定義了一族算法(業(yè)務規(guī)則);
- 封裝了每個算法;
- 這族的算法可互換代替(interchangeable)。
??直接用的書中的例子,感覺還可以,可能會重構此文。
??我們通過一個鴨子應用來了解一下這個模式。。。

模擬鴨子應用中的問題
??假設現(xiàn)在你的公司做了一套模擬鴨子游戲:SimDuck。游戲中出現(xiàn)了各種鴨子,一邊游泳嬉水,一邊呱呱叫。該系統(tǒng)內部設計使用了標準的OO技術,設計了一個鴨子超類(SuperClass),并讓各種鴨子繼承此超類。

??過了幾個月,行業(yè)間其他公司冒出了很多鴨子游戲,公司為了將競爭對手拋在后頭,需要將該游戲加個功能:讓鴨子飛起來。
你:這還不簡單,我在Duck類加個fly()方法,然后所有的鴨子都會繼承fly(),不就搞定了?
但是,可怕的問題發(fā)生了?。?!
??你忽略了一件事,并非所有種類的鴨子都會飛,比如橡皮鴨。當我們?yōu)?code>Duck超類加上了新的行為,會使某些子類也具有這個不恰當?shù)男袨?。我們對代碼做的局部修改,影響層面可能不只是局部。
??繼承不是可以重寫嗎?我們將fly()方法覆蓋重寫不就行了?可是,如果又有個木頭鴨呢,它不會飛也不會叫,我們又要覆蓋重寫?出現(xiàn)更多的其他鴨子的,別的鴨子可能嘎嘎叫呢?還繼續(xù)覆蓋重寫?從這里可以看出,利用繼承來提供Duck的行為,會出現(xiàn)下列問題:
- 代碼在多個子類中重復;
- 難以得知所有鴨子的全部行為;
- 運行時的行為不容易改變;
- 改一發(fā)而動全身,造成其他鴨子不想要的改變。
利用接口如何?
??我們可以把fly()取出來,放進一個Flyable接口中,這樣一來,只有會飛的鴨子才實現(xiàn)此接口。同樣我們也可以設置一個Quackable接口讓會叫的鴨子實現(xiàn)該接口。

??雖然接口可以解決一部分問題(不會飛的橡皮鴨和不會叫的木頭鴨),但是卻造成代碼無法復用,這只是治標卻不治本。
??現(xiàn)在我們知道使用繼承有一些缺失,因為改變鴨子的行為會影響所有種類的鴨子,這行不通。用接口一開始還可以,解決了問題,但接口沒有具體的代碼實現(xiàn),所以繼承接口的方式無法使代碼能復用。這意味著:無論何時你需要修改某個行為,你必須得往下追蹤并修改每一個定義此行為的類,一不小心,可能造成新的錯誤。
??幸運地,有一個
設計原則,正適用于此狀況:找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的代碼混在一起。??換句話說,如果每次新的需求一來,都會變化到某方面的代碼,那么你就可以確定,這部分的代碼需要被抽出來,和其他聞風不動的代碼有所區(qū)隔。
??下面是這個原則的另一個思考方式:「把會變化的部分取出并封裝起來,以便以后可以輕易地擴充此部分,而不影響不需要變化的其他部分」。
??這樣的概念很簡單,幾乎是每個設計模式背后的精神所在。所有的模式都提供了一套方法讓「系統(tǒng)中的某部分改變不會影響其他部分」。代碼變化之后,出其不意的部分變得很少,系統(tǒng)變得更有彈性。
分開變化和不會變化的部分
??現(xiàn)在,為了要分開「變化和不會變化的部分」,我們準備建立兩組類(完全遠離 D u c k類),一個是fly相關的,一個是quack相關的,每一組類將實現(xiàn)各自的動作。
??我們知道Duck類內的fly ( )和quack( )會隨著鴨子的不同而改變。
為了要把這兩個行為從Duck類中分開,我們將把它們自 Duck類中取出,建立一組新類代表每個行為。

如何設計類實現(xiàn)飛行和呱呱叫的行為?
??我們希望一切能有彈性,畢竟,正是因為一開始的鴨子行為沒有彈性,才讓我們走上現(xiàn)在這條路。我們還想能夠「指定」行為到鴨子的實例,比方說,想要產(chǎn)生綠頭鴨實例,并指定特定「類型」的飛行行為給它。干脆順便讓鴨子的行為可以動態(tài)地改變好了。換句話說,我們應該在鴨子類中包含設定行為的方法,就可以在「運行時」動態(tài)地「改變」綠頭鴨的飛行行為。
??有了這些目標要達成,接著看看第二個設計原則:針對接口編程,而不是針對實現(xiàn)編程。
??我 們 利 用 接 口 代 表 每 個 行 為 , 比 方 說 ,FlyBehavior與QuackBehavior,而行為的每個實現(xiàn)都必須實現(xiàn)這些接口之一。
??所以這次鴨子類不會負責實現(xiàn) Flying與Quacking 接口,反而是由其他類專門實現(xiàn)FlyBehavior與QuackBehavior,這就稱為
「行為」類。由行為類實現(xiàn)行為接口,而不是由Duck類實現(xiàn)行為接口。
??這樣的作法迥異于以往,以前的作法是:行為是繼承 D u c k超類的具體實現(xiàn)而來,或是繼承某個接口并由子類自行實現(xiàn)而來。這兩種作法都是依賴于「實現(xiàn)」,我們被實現(xiàn)綁得死死的,沒辦法更改行為(除非寫更多代碼)。
??在我們的新設計中,鴨子的子類將使用接口( FlyBehavior與QuackBehavior)所表示的行為,所以實際的「實現(xiàn)」不會被綁死在鴨子的子類中。(換句話說,特定的實現(xiàn)代碼位于實現(xiàn)FlyBehavior與QuackBehavior的特定類中)。
實現(xiàn)鴨子的行為

整合鴨子的行為
??關鍵在于,鴨子現(xiàn)在會將飛行和呱呱叫的動作,「委托」(delegate)別人處理,而不是使用定義在自己類(或子類)內的方法。
??作法是這樣的:
①首 先 , 在 鴨 子 中 「 加 入 兩 個 實 例 變 量 」 , 分 別 為 FlyBehavior與QuackBehavior,聲明為接口類型(而不是具體類實現(xiàn)類型),每個變量會利用多態(tài)的方式在運行時引用正確的行為類型(例如:FlyWithWings)。我們也必須將 D u c k類與其所有子類中的 f l y ( )與 q u a c k ( )移除,因為這些行為已經(jīng)被搬移到FlyBehavior與QuackBehavior類中了。
我們用 performFly()和 performQuack()取代 Duck類中的fly() 與quack()。
稍后你就知道為什么。
②父類鴨子的代碼:
public class Duck{
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public void performFly(){
flyBehavior.fly();
}
public void performQuack(){
quackBehavior.quack();
}
}
③現(xiàn) 在 來 關 心 「 如 何 設 定flyBehavior與quackBehavior的實例變量」。看看 RedDuck類:
public class RedDuck extends Duck{
public RedDuck(){
flyBehavior = new GaGaQuack();
quackBehavior = new FlyWithWings();
}
}
??所以,紅頭鴨會嘎嘎叫,而不是吱吱叫,或叫不出聲,紅頭鴨還會用翅膀飛。這是怎么做到的呢?當RedDuck實例化時,它的構造器會把繼承來的flyBehavior與quackBehavior實例變量初始化為相應接口的具體實現(xiàn)類。
??可是這樣還是有個問題:紅頭鴨創(chuàng)建時就被定義了飛和叫的行為,這是不是太過于死板,不夠emmm,靈活?是的,如果紅頭鴨病了呢,嗓子叫不出來,變成了啞巴呢。。。它不就不會叫了呀。。。卻是會發(fā)生這種情況的呀!沒事,我們還有解決辦法:動態(tài)設定行為
動態(tài)設定行為
①在Duck類中,加入下面的方法:
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
??從此以后,我們就可以隨時調用這兩個方法改變鴨子的行為。比如將前面變成啞巴的鴨子的叫聲變成不會叫。。。
②現(xiàn)在我們制造一個新的鴨子模型鴨(一開始它是不會飛的):
public class ModelDuck extends Duck {
public ModelDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}
public void display() {
System.out.println("我是一個模型鴨");
}
}
③建立一個新的FlyBehavior的實現(xiàn)類:
public class FlyRocketPowered implements FlyBehavior {
public void fly() {
System.out.println("我用火箭飛了起來");
}
}
//FlyBehavior 接口
public interface FlyBehavior {
public void fly();
}
③創(chuàng)建模型鴨,并設置其飛行行為帶上火箭:
Duck model = new ModelDuck();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
封裝行為的大局觀
??好,我們已經(jīng)深入鴨子模擬器的設計,該是將頭探出水面,呼吸空氣的時候了?,F(xiàn)在就來看看整體的格局。
??下面是整個重新設計后的類結構,你所期望的一切都有:鴨子繼承Duck,
??飛行行為實現(xiàn)FlyBehavior 接口,呱呱叫行為實現(xiàn)QuackBehavior接口。
也 請 注 意 , 我 們 描 述 事 情 的 方 式 也 稍 有 改 變 。 不 再 把 鴨 子 的 行 為 說成「一組行為」,我們開始把行為想成是「一族算法」。想想看,在游戲設計中,算法代表鴨子能做的事(不同的叫法和飛行法),這樣的作法也能用于用一群類計算不同國家的銷售稅金。
請?zhí)貏e注意類之間的『關 系』。拿一枝筆 ,把下面圖形中的每個箭頭標上適當?shù)年P系,關系可以是is-a(是一個)、has - a(有一個)、implements(實現(xiàn))。

『有一個』可能比『是一個』更好
??『 有 一 個 』 關 系 相 當 有 趣 : 每 一 鴨 子 都 有 一 個飛的行為和叫的行為,讓鴨子將飛行和呱呱叫委托它們代為處理。
當你將兩個類結合起來使用,如同本例一般,這就是組合(composition )。這種作法和『繼承』不同的地方在于,鴨子的行為不是繼承而來,而是和適當?shù)男袨閷ο蟆航M合』而來。
這是一個很重要的技巧。其實是使用了我們的第三個設計原則:多用組合,少用繼承。
??如你所見,使用組合建立系統(tǒng)具有很大的彈性,不僅可將 算 法 族 封 裝 成 類 , 更 可 以 『 在 運 行 時 動 態(tài) 地 改 變 行為』,只要組合的行為對象,符合正確的接口標準即可。
組合用在『許多』設計模式中,它有優(yōu)點也有缺點。
沒錯,這就是策略(Strategy)模式。
??本章使用到了3個設計原則:
- 分離程序中變與不變的部分
- 針對接口編程,不針對實現(xiàn)編程
- 多用組合,少用繼承
參考資料
《HeadFirst設計模式》第一章