設計模式之一:策略模式(Strategy pattern)
定義:
定義了算法族,分別封裝起來,讓它們之間可以互相替換,此模式讓算法的變化獨立於使用算法的客戶。
The strategy pattern definesa family of algorithms, encapsulates each one, and makes them interchangeable.Strategy lets the algorithm vary independently from clients that use it.
要點:
1. 知道OO基礎,並不足以讓你設計出良好的OO係統。
2. 良好的OO設計必須具備可複用、可擴充、可維護三個特性。
3. 模式可以讓我們建造出具有良好OO設計質量的係統。
4. 模式被認為是曆經驗證的OO設計經驗。
5. 模式不是代碼,而是針對設計問題的通用解決方案。你可以把它們應用到特定的應用中。
6. 模式不是被發明,而是被發現。
7. 大多數的模式和原則,都著眼於軟件變化的主題。
8. 大多數的模式都允許係統局部改變獨立於其他部分。
9. 我們常把係統中會變化的部分抽出來封裝。
10. 模式讓開發人員之間有共享的語言,能夠最大化溝通的價值。
模式介紹
策略模式是一個很簡單的模式,也是一個很常用的模式,可謂短小精悍。廢話不多說了,下麵開始介紹策略模式。
Joe的公司做了一套相當成功的模擬鴨子遊戲:SimUDuck.遊戲中會出現各種鴨子,一邊遊泳戲水,一遍呱呱叫。係統的核心類圖如下所示:
如圖所示,在Duck基類裏實現了公共的quack()和swim()方法,而MallardDuck和RedheadDuck可以分別覆蓋實現自己的display()方法,這樣既重用了公共的部分,又支持了不同子類的個性化擴展。
隨著公司競爭壓力的加劇,公司主管決定設計出會飛的鴨子來將競爭對手拋在後頭。Joe拍著胸脯保證一個星期就可以解決。Joe發現隻要在Duck類中加上fly()方法,然後所有鴨子都會繼承fly().
Joe很高興的帶著自己的產品到股東會議上去展示,有很多“橡皮鴨子”飛來飛去。這是怎麼回事?原來Joe忽略了一件事:並非Duck所有的子類都會飛。Joe在Duck超類中加上新的行為,會使得某些並不適合該行為的子類 也具有該行為。現在可好了!SimUDuck程序中有了一個無生命的會飛的東西。他意會到了一件事:當涉及“維護”時,為了“複用”目的而使用繼承,結局並不完美。Joe很鬱悶!他突然想到:如果在RubberDuck類裏把fly()方法重寫一下會如何?在RubberDuck類的fly()裏讓橡皮鴨子什麼都不做,不就一切OK了嗎!那以後再增加一個木頭鴨子呢?它不會飛也不會叫,那不是要再重寫quack()和fly()方法,以後再增加其它特殊的鴨子都要這樣,這不是太麻煩了,而且也很混亂。
最終,Joe認識到使用繼承不是辦法,因為他的上司通知他,董事會決定以後每6個月就會升級一次係統,以應對市場競爭,所以未來的變化會很頻繁,而且還不可預知。如果以後靠逐個類去判斷是否重寫了quack()或fly()方法來應對變化,顯然混不下去!
那麼用接口能不能解決這個問題嗎?把fly()從超類中取出來,放進一個“Flyable接口”中。這麼一來隻有會飛的鴨子才實習該接口。同樣的方式,也可以用來設計一個“Quackable接口”,因為不是所以的鴨子都會叫。
但是這種方法會出現代碼無法重用的問題,如果鴨子的類特別多的話,就這麼幾個鴨子還好說,但是我們有幾十、上百個鴨子的時候你怎麼辦?如果某個方法要做一點修改,就需要重複修改上百遍。
嗬嗬!如果你是Joe,你該怎麼辦?
我們知道,並不是所有的鴨子都會飛、會叫,所以繼承不是正確的方法。但是雖然上麵的使用Flyable接口的方法,可以解決部分問題(不再有會飛的橡皮鴨子),但是這個解決方案卻徹底破壞了重用,它帶來了另一個維護的噩夢!而且還有一個問題我們前麵沒有提到,難道所有的鴨子的飛行方式、叫聲等行為都是一模一樣的嗎?不可能吧!
說到這裏,為了能幫助Joe擺脫困境,我們有必要先停下來,重新回顧一些麵向對象設計原則。請您告訴我:“什麼東西是在軟件開發過程中是恒定不變的?”,您想到了嗎?對,那就是變化本身,正所謂“計劃沒有變化快”,所以直麵“變化這個事實”才是正道!Joe麵對的問題是,鴨子的行為在子類裏持續不斷地改變,所以讓所有的子類都擁有基類的行為是不適當的,而使用上麵的接口的方式,又破壞了代碼重用。現在就需要用到我們的第一個設計原則:
找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的代碼混在一起。
換句話說,如果每次心的需求一來,都會使某方麵的代碼發生變化,那麼你就可以確定,這部分的代碼需要被抽出來,和其他文檔的代碼有所區分。這個原則的另一種思考方式是:把會變化的部分取出並封裝起來,以便以後可以輕易地蓋棟或擴充此部分,而不影響不需要變化的其他部分。
OK!現在我們已經有了一條設計原則,那麼Joe的問題怎麼辦呢?就鴨子的問題來說,變化的部分就是子類裏的行為。所以我們要把這部分行為封裝起來,省得它們老惹麻煩!從目前的情況看,就是fly()和quack()行為總是不老實,而swim()行為是很穩定的,這個行為是可以使用繼承來實現代碼重用的,所以,我們需要做的就是把fly()和quack()行為從Duck基類裏隔離出來。我們需要創建兩組不同的行為,一組表示fly()行為,一組表示quack()行為。為什麼是兩組而不是兩個呢?因為對於不同的子類來說,fly()和quack()的表現形式都是不一樣的,有的鴨子嘎嘎叫,有的卻呷呷叫。有了這兩組行為,我們就可以組合出不同的鴨子,例如:我們可能想要實例化一個新的MallardDuck(野鴨)實例,並且給它初始化一個特殊類型的飛行行為(野鴨飛行能力比較強)。那麼,如果我們可以這樣,更進一步,為什麼我們不可以動態地改變一個鴨子的行為呢?換句話說,我們將在Duck類裏包含行為設置方法,所以我們可以說在運行時改變MallardDuck的飛行行為,這聽起來更酷更靈活了!那麼我們到底要怎麼做呢?回答這個問題,先要看一下我們的第二個設計原則:
麵向接口編程,而不是針對實現編程。
針對接口編程真正的意思是針對超類型編程。“針對接口編程”關鍵就在多態。利用多態,程序可以針對超類型編程,執行時會根據實際狀況執行到真正的行為,不會被蚌寺在超類型的行為上。“針對超類型編程”這句話,可以更明確地說成“變量的生命類型應該是超類型,通常是一個抽象類或者是一個接口,如此,隻要是具體實現此超類型的類所產生的對象,都可以指定給這個變量。這也意味著,聲明類時不用理會以後執行時的真正對象類型”
根據麵向接口編程的設計原則,我們應該用接口來隔離鴨子問題中變化的部分,也就是鴨子的不穩定的行為(fly()、quack())。
第一步:我們要給Duck類增加兩個接口類型的實例變量,分別是flyBehavior和quackBehavior,它們其實就是新的設計裏的“飛行”和“叫喚”行為。每個鴨子對象都將會使用各種方式來設置這些變量,以引用它們期望的運行時的特殊行為類型(使用橫著飛,吱吱叫,等等)。
第二步:我們還要把fly()和quack()方法從Duck類裏移除,因為我們已經把這些行為移到FlyBehavior和QuackBehavior接口裏了。我們將使用兩個相似的PerformFly()和PerformQuack()方法來替換fly()和qucak()方法,後麵你會看到這兩個新方法是如何起作用的。
第三步:我們要考慮什麼時候初始化flyBehavior和quackBehavior變量。最簡單的辦法就是在Duck類初始化的時候同時初始化他們。但是我們這裏還有更好的辦法,就是提供兩個可以動態設置變量值的方法SetFlyBehavior()和SetQuackBehavior(),那麼就可以在運行時動態改變鴨子的行為了。
修改後的Duck類如下圖所示:
測試代碼:
public abstract class Duck { FlyBehavior flyBehavior; QuackBehavior quackBehavior; public Duck(){} public void performFly() { flyBehavior.fly(); } public void performQuack() { quackBehavior.quack(); } public void swim() { System.out.println("All ducks float,even decoys!"); } public void setFlyBehavior(FlyBehavior fb) { flyBehavior = fb; } public void setQuackBehavior(QuackBehavior qb) { quackBehavior = qb; } }
public class ModelDuck extends Duck{ public ModelDuck() { flyBehavior = new FlyNoWay(); quackBehavior = new Quack(); } public void display() { System.out.println("I'm a model duck"); } }
public class MallardDuck extends Duck{ public MallardDuck() { quackBehavior = new Quack(); flyBehavior = new FlyWithWings(); } public void display() { System.out.println("I'm a real Mallard duck"); } }
public class MuteQuack implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("<<Silence>>"); } }
public interface FlyBehavior { public void fly(); }
public class FlyNoWay implements FlyBehavior { public void fly(){ System.out.println("I can't fly"); } }
public class FlyRocketPowered implements FlyBehavior { @Override public void fly() { // TODO Auto-generated method stub System.out.println("I'm flying with a rocket!"); } }
public class FlyWithWings implements FlyBehavior { public void fly(){ System.out.println("I'm flying"); } }
public interface QuackBehavior { public void quack(); }
public class Quack implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("Quack"); } }
public class Squeak implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("Squeak"); } }
public class MiniDuckSimulator { public static void main(String[] args){ Duck mallard = new MallardDuck(); mallard.performQuack(); mallard.performFly(); Duck model = new ModelDuck(); model.performQuack(); model.performFly(); model.setFlyBehavior(new FlyRocketPowered()); model.performFly(); } }
程序運行結果如下:
應用場景和優缺點
上麵我們已經看過了Strategy模式的詳細介紹,下麵我們再來簡單說說這個模式的優缺點吧!怎麼說呢,人無完人,設計模式也不是萬能的,每一個模式都有它的使命,也就是說隻有在特定的場景下才能發揮其功效。我們要使用好模式,就必須熟知各個模式的應用場景。
對於Strategy模式來說,主要有這些應用場景:
1、 多個類隻區別在表現行為不同,可以使用Strategy模式,在運行時動態選擇具體要執行的行為。(例如FlyBehavior和QuackBehavior)
2、 需要在不同情況下使用不同的策略(算法),或者策略還可能在未來用其它方式來實現。(例如FlyBehavior和QuackBehavior的具體實現可任意變化或擴充)
3、 對客戶(Duck)隱藏具體策略(算法)的實現細節,彼此完全獨立。
對於Strategy模式來說,主要有如下優點:
1、 提供了一種替代繼承的方法,而且既保持了繼承的優點(代碼重用)還比繼承更靈活(算法獨立,可以任意擴展)。
2、 避免程序中使用多重條件轉移語句,使係統更靈活,並易於擴展。
3、 遵守大部分GRASP原則和常用設計原則,高內聚、低偶合。
對於Strategy模式來說,主要有如下缺點:
1、 因為每個具體策略類都會產生一個新類,所以會增加係統需要維護的類的數量。
封裝行為的大局觀

最後更新:2017-04-04 07:03:51