策略模式的學習(轉載的,我增加了Java代碼實現)

原文地址:http://blog.csdn.net/cuoguo1111/article/details/1503480

Java語言版下載地址:http://download.csdn.net/detail/carterjin/4625805 (支持下老弟,收取資源費1分~~~偷笑

C#語言版下載地址:http://www.cnblogs.com/Files/justinw/DesignPatterns.Strategy.Ducks.rar



萬事開頭難,最近對這句話體會深刻!這篇文章是這個系列正式開始介紹設計模式的第一篇,所以肩負着確定這個系列風格的歷史重任,它在我腦袋裏默默地醞釀了好多天,卻只搜刮出了一點兒不太清晰的輪廓,可是時間不等人,以後再多“迭代”幾次吧!在前面的隨筆裏,我已經提到了,這個系列準備以《Head First Design Patterns》的結構爲主線,所以每個模式的核心故事都是取材於此書,在此再次聲明一下。不管怎樣,宗旨是爲了跟大家一起循序漸進地去認識設計模式。

上一篇:模式和原則,得到很多朋友的支持和鼓勵,這裏再次深表感謝。這裏我還是想呼籲一下,希望大家看過後多提寶貴意見,反對意見更好,關鍵是我們在互動中可以共同進步,因爲經驗告訴我討論(爭論更甚)出來的火花,總是印象最深刻的。

其實策略模式是一個很簡單的模式,也是一個很常用的模式,可謂短小精悍。我在介紹這個模式的同時,爲了加深大家對OO的理解,還會反覆強調前面講過的設計原則和GRASP模式。這個系列的文章前後多少會有一些關聯的連續性,但是單獨一篇文章針對單一模式也一定是獨立的,所以不論大家想從前往後連續看也好,還是挑喜歡的跳着看,都沒有問題。

“羅嗦了這麼多,太唐僧了吧,快點開始吧…”(爛西紅柿和臭雞蛋從四面八方飛來)

模擬鴨子

Joe是一名OO程序員,他爲一家開發模擬鴨子池塘遊戲的公司工作,該公司的主要產品是一種可以模擬展示多種會游泳和呷呷叫的鴨子的遊戲。這個遊戲是使用標準的面向對象技術開發的,系統裏所有鴨子都繼承於Duck基類,系統的核心類圖如下:

Duck基類裏實現了公共的quack()swim()方法,而MallardDuckRedheadDuck可以分別覆蓋實現自己的display()方法,這樣即重用了公共的部分,又支持不同子類的個性化擴展。從目前的情況看,這是一個很好的設計,哈!

但是,商場如戰場,不進則退。Joe的公司最近的日子不好過,盜版氾濫,再加上競爭對手的圍追堵劫,已經拖欠好幾個月工資了。因此,公司高層在一次集體“腐敗”後,決定一定要給系統增加一些超玄的功能,以徹底擊垮競爭對手。經過董事會討論,最終覺得如果能讓鴨子飛起來,那麼一定可以給對手致命一擊。於是Joe的上司對董事們拍着胸脯說:“這沒有問題,Joe是一個OO程序員,這對他來說太簡單了!我們保證一週內結束戰鬥。”

接到任務的Joe絲毫不敢怠慢,研究了上級的指示以後,發現只要在Duck裏增加一個fly()方法就可以搞定了,這樣所有繼承Duck的鴨子就都擁有了會飛的能力,哈!這回獎金有盼頭啦!改進後的系統類圖如下:

的上司很高興,帶着新產品給董事們演示去了……


Joe

……

Joe的上司:“我正在給董事們演示你會飛的鴨子,但是怎麼有很多橡皮鴨子也在四處亂飛呢?你在耍我嗎?你還想不想混啦?!”(此處省略粗話100)

Joe被嚇壞了,到手的獎金泡湯了!冷靜下來的Joe發現,原來在Duck類裏增加的方法,也同樣被繼承於Duck的RubberDuck類繼承了,所以就有了會飛的橡皮鴨子,這是嚴重違反該系統“真實模擬各種鴨子”的原則的!那麼該怎麼辦呢?Joe很鬱悶!他突然想到:如果在RubberDuck類裏把fly()方法重寫一下會如何?在RubberDuck類的fly()裏讓橡皮鴨子什麼都不做,不就一切OK了嗎!那以後再增加一個木頭鴨子呢?它不會飛也不會叫,那不是要再重寫quack()和fly()方法,以後再增加其它特殊的鴨子都要這樣,這不是太麻煩了,而且也很混亂。

最終,Joe認識到使用繼承不是辦法,因爲他的上司通知他,董事會決定以後每6個月就會升級一次系統,以應對市場競爭,所以未來的變化會很頻繁,而且還不可預知。如果以後靠逐個類去判斷是否重寫了quack()或fly()方法來應對變化,顯然混不下去!

Joe這時很迷惑,爲什麼屢試不爽的繼承,在系統維護升級的時候,無法很好地支持重用呢?)

那麼使用接口怎麼樣?我可以把fly()方法放在接口裏,只有那些會飛的鴨子才需要實現這個接口,最好把quack()方法也拿出來放到一個接口裏,因爲有些鴨子是不會叫的。就像下面這樣:

的上司知道後怒了:“你這樣做難道是希望所有需要quack()和fly()方法的鴨子都去重複實現這兩個方法的功能嗎?就這麼幾個鴨子還好說,但是我們有幾十、上百個鴨子的時候你怎麼辦?如果某個方法要做一點修改,難道你要重複修改上百遍嗎?你是不是瘋啦?”

呵呵!如果你是Joe,你該怎麼辦?

我們知道,並不是所有的鴨子都會飛、會叫,所以繼承不是正確的方法。但是雖然上面的使用Flyable接口的方法,可以解決部分問題(不再有會飛的橡皮鴨子),但是這個解決方案卻徹底破壞了重用,它帶來了另一個維護的噩夢!而且還有一個問題我們前面沒有提到,難道所有的鴨子的飛行方式、叫聲等行爲都是一模一樣的嗎?不可能吧!

說到這裏,爲了能幫助Joe擺脫困境,我們有必要先停下來,重新回顧一些面向對象設計原則。請您告訴我:“什麼東西是在軟件開發過程中是恆定不變的?”,您想到了嗎?對,那就是變化本身,正所謂“計劃沒有變化快”,所以直面“變化這個事實”纔是正道!Joe面對的問題是,鴨子的行爲在子類裏持續不斷地改變,所以讓所有的子類都擁有基類的行爲是不適當的,而使用上面的接口的方式,又破壞了代碼重用。現在就需要用到我們的第一個設計原則:

Identify the aspects of your application that vary and separate them from what stays the same.(找到系統中變化的部分,將變化的部分同其它穩定的部分隔開。)

換句話說就是:“找到變化並且把它封裝起來,稍後你就可以在不影響其它部分的情況下修改或擴展被封裝的變化部分。” 儘管這個概念很簡單,但是它幾乎是所有設計模式的基礎,所有模式都提供了使系統裏變化的部分獨立於其它部分的方法。

OK!現在我們已經有了一條設計原則,那麼Joe的問題怎麼辦呢?就鴨子的問題來說,變化的部分就是子類裏的行爲。所以我們要把這部分行爲封裝起來,省得它們老惹麻煩!從目前的情況看,就是fly()和quack()行爲總是不老實,而swim()行爲是很穩定的,這個行爲是可以使用繼承來實現代碼重用的,所以,我們需要做的就是把fly()和quack()行爲從Duck基類裏隔離出來。我們需要創建兩組不同的行爲,一組表示fly()行爲,一組表示quack()行爲。爲什麼是兩組而不是兩個呢?因爲對於不同的子類來說,fly()和quack()的表現形式都是不一樣的,有的鴨子嘎嘎叫,有的卻呷呷叫。有了這兩組行爲,我們就可以組合出不同的鴨子,例如:我們可能想要實例化一個新的MallardDuck(野鴨)實例,並且給它初始化一個特殊類型的飛行行爲(野鴨飛行能力比較強)。那麼,如果我們可以這樣,更進一步,爲什麼我們不可以動態地改變一個鴨子的行爲呢?換句話說,我們將在Duck類裏包含行爲設置方法,所以我們可以說在運行時改變MallardDuck的飛行行爲,這聽起來更酷更靈活了!那麼我們到底要怎麼做呢?回答這個問題,先要看一下我們的第二個設計原則:

Program to an interface, not an implementation.(面向接口編程,而不要面向實現編程。)

嘿!對於這個原則,不論是耳朵還是眼睛,是不是都太熟悉了!“接口”這個詞已經被賦予太多的含義,搞的大家一說點兒屁事就滿嘴往外蹦“接口”。那麼它到底是什麼意思呢?我們這裏說的接口是一個抽象的概念,不侷限於語言層面的接口(例如C#裏的interface)。一個接口也可以是一個抽象類,或者一個基類也可以看作是一種接口的表現形式,因爲基類變量可以用來引用其子類。要點在於,我們在面向接口編程的時候,可以使用多態,那麼實際運行的代碼只依賴於具體的接口(interface,抽象類,基類),而不管這些接口提供的功能是如何實現的,也就是說,接口將系統的不同部分隔離開來,同時又將它們連接在一起。我的神啊!接口真是太偉大了!(爛西紅柿和臭雞蛋從四面八方飛來)

OK!這回該徹底解決Joe的問題了!

根據面向接口編程的設計原則,我們應該用接口來隔離鴨子問題中變化的部分,也就是鴨子的不穩定的行爲(fly()、quack())。我們要用一個FlyBehavior接口表示鴨子的飛行行爲,這個接口可以有多種不同的實現方式,可以“橫”着分,也可以“豎”着分,管它呢!這樣做的好處就是我們將鴨子的行爲實現在一組獨立的類裏,具體的鴨子是通過FlyBehavior這個接口來調用這個行爲的,因爲Duck只依賴FlyBehavior接口,所以不需要管FlyBehavior是如何被實現的。如下面的類圖,FlyBehavior和QuackBehavior接口都有不同的實現方式!

已經暈了,“你說了這麼多,全是大白話,來點代碼行不行,我要C#的!”。說到這裏,我們也該開始徹底改造這個設計了,並會在最後附加部分代碼來幫助大家理解。

第一步:我們要給Duck類增加兩個接口類型的實例變量,分別是flyBehavior和quackBehavior,它們其實就是新的設計裏的“飛行”和“叫喚”行爲。每個鴨子對象都將會使用各種方式來設置這些變量,以引用它們期望的運行時的特殊行爲類型(使用橫着飛,吱吱叫,等等)。

第二步:我們還要把fly()和quack()方法從Duck類裏移除,因爲我們已經把這些行爲移到FlyBehavior和QuackBehavior接口裏了。我們將使用兩個相似的PerformFly()和PerformQuack()方法來替換fly()和qucak()方法,後面你會看到這兩個新方法是如何起作用的。

第三步:我們要考慮什麼時候初始化flyBehavior和quackBehavior變量。最簡單的辦法就是在Duck類初始化的時候同時初始化他們。但是我們這裏還有更好的辦法,就是提供兩個可以動態設置變量值的方法SetFlyBehavior()和SetQuackBehavior(),那麼就可以在運行時動態改變鴨子的行爲了。

下面是修改後的Duck類圖:


這就是策略模式

前面說了那麼多,現在終於到了正式介紹我們今天的主角的時候啦!此刻心情真是好激動啊!其實我們在前面就是使用Strategy模式幫Joe度過了難過,真不知道他發了獎金後要怎麼感謝我們啊。OK!下面先看看官方的定義:

The Strategy Pattern defines a family of algorithms,encapsulates each one,and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.(策略模式定義了一系列的算法,並將每一個算法封裝起來,而且使它們還可以相互替換。策略模式讓算法獨立於使用它的客戶而獨立變化。)

怎麼樣,有了前面Joe的經歷,這個定義理解起來還不那麼太費勁吧?我想凡是認真看到這裏的人,應該都能理解的。那麼下面再畫蛇添足地羅嗦幾句,給那些還不太理解的朋友一個機會吧。J

應用場景):

l 需要使用ConcreteStrategy提供的算法。

l 內部維護一個Strategy的實例。

l 負責動態設置運行時Strategy具體的實現算法。

l 負責跟Strategy之間的交互和數據傳遞。

Strategy(抽象策略類)

l 定義了一個公共接口,各種不同的算法以不同的方式實現這個接口,Context使用這個接口調用不同的算法,一般使用接口或抽象類實現。

ConcreteStrategy(具體策略類)

l 實現了Strategy定義的接口,提供具體的算法實現。

還不理解?!我的神啊!那再看看下面的順序圖吧,這是最後的機會啦!


應用場景和優缺點

上面我們已經看過了Strategy模式的詳細介紹,下面我們再來簡單說說這個模式的優缺點吧!怎麼說呢,人無完人,設計模式也不是萬能的,每一個模式都有它的使命,也就是說只有在特定的場景下才能發揮其功效。我們要使用好模式,就必須熟知各個模式的應用場景。

對於Strategy模式來說,主要有這些應用場景:

1、 多個類只區別在表現行爲不同,可以使用Strategy模式,在運行時動態選擇具體要執行的行爲。(例如FlyBehaviorQuackBehavior)

2、 需要在不同情況下使用不同的策略(算法),或者策略還可能在未來用其它方式來實現。(例如FlyBehaviorQuackBehavior的具體實現可任意變化或擴充)

3、 對客戶(Duck)隱藏具體策略(算法)的實現細節,彼此完全獨立。

對於Strategy模式來說,主要有如下優點:

1、 提供了一種替代繼承的方法,而且既保持了繼承的優點(代碼重用)還比繼承更靈活(算法獨立,可以任意擴展)

2、 避免程序中使用多重條件轉移語句,使系統更靈活,並易於擴展。

3、 遵守大部分GRASP原則和常用設計原則,高內聚、低偶合。

對於Strategy模式來說,主要有如下缺點:

1、 因爲每個具體策略類都會產生一個新類,所以會增加系統需要維護的類的數量。

備註:關於場景和優缺點,上面肯定說得不夠全面,歡迎大家來補充。

.NET框架裏的應用

Strategy模式的應用非常廣泛,也許大家有意無意之間一直都在使用。這裏舉一個.NET框架裏使用Strategy模式的例子,象這樣的例子其實還有很多,只要大家細心體會就一定會發現的。

如果寫過程序,那麼ArrayList類肯定都會用過吧,那麼它的Sort方法想必大家也一定不陌生了。Sort方法的定義如下:

public virtual void Sort (IComparer comparer)

可以看到Sort方法接收一個IComparer類型的參數,那麼這個IComparer接口是做什麼用的呢?下面我們看一段程序,下面的代碼示例演示如何使用默認比較器和一個反轉排序順序的自定義比較器,對 ArrayList 中的值進行排序。(完全引自MSDN:ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.NETDEVFX.v20.chs/cpref2/html/M_System_Collections_ArrayList_Sort_1_a2d90598.htm)

1using System;
2using System.Collections;
3
4publicclass SamplesArrayList {
5
6publicclass myReverserClass : IComparer {
7
8// Calls CaseInsensitiveComparer.Compare with the parameters reversed.
9int IComparer.Compare( Object x, Object y ) {
10return( (new CaseInsensitiveComparer()).Compare( y, x ) );
11 }

12
13 }

14
15publicstaticvoid Main() {
16
17// Creates and initializes a new ArrayList.
18 ArrayList myAL =new ArrayList();
19 myAL.Add( "The" );
20 myAL.Add( "quick" );
21 myAL.Add( "brown" );
22 myAL.Add( "fox" );
23 myAL.Add( "jumps" );
24 myAL.Add( "over" );
25 myAL.Add( "the" );
26 myAL.Add( "lazy" );
27 myAL.Add( "dog" );
28
29// Displays the values of the ArrayList.
30 Console.WriteLine( "The ArrayList initially contains the following values:" );
31 PrintIndexAndValues( myAL );
32
33// Sorts the values of the ArrayList using the default comparer.
34 myAL.Sort();
35 Console.WriteLine( "After sorting with the default comparer:" );
36 PrintIndexAndValues( myAL );
37
38// Sorts the values of the ArrayList using the reverse case-insensitive comparer.
39 IComparer myComparer =new myReverserClass();
40 myAL.Sort( myComparer );
41 Console.WriteLine( "After sorting with the reverse case-insensitive comparer:" );
42 PrintIndexAndValues( myAL );
43
44 }

45
46publicstaticvoid PrintIndexAndValues( IEnumerable myList ) {
47int i =0;
48foreach ( Object obj in myList )
49 Console.WriteLine( "/t[{0}]:/t{1}", i++, obj );
50 Console.WriteLine();
51 }

52
53}

54
55
56/**//*
57This code produces the following output.
58The ArrayList initially contains the following values:
59 [0]: The
60 [1]: quick
61 [2]: brown
62 [3]: fox
63 [4]: jumps
64 [5]: over
65 [6]: the
66 [7]: lazy
67 [8]: dog
68
69After sorting with the default comparer:
70 [0]: brown
71 [1]: dog
72 [2]: fox
73 [3]: jumps
74 [4]: lazy
75 [5]: over
76 [6]: quick
77 [7]: the
78 [8]: The
79
80After sorting with the reverse case-insensitive comparer:
81 [0]: the
82 [1]: The
83 [2]: quick
84 [3]: over
85 [4]: lazy
86 [5]: jumps
87 [6]: fox
88 [7]: dog
89 [8]: brown
90*/

怎麼樣,大家看出來了吧,其實在這段代碼裏,ArrayList相當於Strategy模式中的Context(應用場景)部分,而IComparer相當於Strategy(抽象策略類)部分,myReverserClass相當於ConcreteStrategy(具體策略類)部分。我們這裏拋開myReverserClass類的Compare方法如何具體實現不談,我們只要知道這是一個具體策略類,它提供了應用場景需要的具體算法,它實現了抽象策略類接口,而應用場景通過抽象策略類動態調用到了具體策略類中的算法。哈!所以這是一個十分典型的Strategy模式的應用。

基於這個符合Strategy模式的結構,我們還可以提供很多種自定義的具體策略類的實現,只要這些類實現了IComparer接口,就可以在運行時動態設置給ArrayList類的Sort方法,在Sort方法中會根據具體策略類實現的比較算法規則來對ArrayList中的數據進行排序。

最後一個設計原則

關於Strategy模式的故事講到這裏,應該基本OK啦!下面我們再聊些更高層次的東西。什麼是更高層次的東西?嘿!當然是設計原則了!在前面總結Strategy模式的優點的時候我們提到過,Strategy模式不僅保留了繼承的優點,而且還提供了更靈活的擴展能力。爲什麼會這樣呢?Strategy模式是怎麼做到這一點的呢?哈!這是因爲它“上面有人”啊!誰啊?它就是我們下面要介紹的重量級設計原則:

Favor composition over inheritance.(優先使用對象組合,而非類繼承)

關於組合和繼承,我們只要這樣來理解即可:組合是一種“HAS-A”關係,而繼承是一種“IS-A”關係。很明顯“HAS-A”要比“IS-A”更靈活一些。也就是說在創建系統的時候,我們應該優先使用對象組合,因爲它不僅可以給你提供更多靈活性和擴展性,而且還使你可以在運行時改變行爲(組合不同的對象),這簡直是酷斃了!但是也不是說繼承就是不能用,只是說應該把繼承應用在相對更穩定,幾乎沒有變化的地方,例如前面的Duck類裏的Swim()方法,因爲可以肯定所有鴨子一定都會游泳,所以就沒有必要給這個行爲提供基於Strategy模式的實現方式,因爲那樣做除了是程序更復雜以外,沒有什麼意義。

Context(

最後大家再看看演示代碼,因爲代碼比較多,就不貼出來了,大家可以下載後參考:。下面是演示代碼的執行結果:
我們再看看整個設計修改後的類圖:
Joe


Joe
如圖所示,在
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章