設計模式隨筆系列:鴨子-策略模式(Strategy)[原]
鴨子-策略模式(Strategy)
前言
萬(wàn)事開(kāi)頭難,最近對這句話(huà)體會(huì )深刻!這篇文章是這個(gè)系列正式開(kāi)始介紹設計模式的第一篇,所以肩負著(zhù)確定這個(gè)系列風(fēng)格的歷史重任,它在我腦袋里默默地醞釀了好多天,卻只搜刮出了一點(diǎn)兒不太清晰的輪廓,可是時(shí)間不等人,以后再多“迭代”幾次吧!在前面的隨筆里,我已經(jīng)提到了,這個(gè)系列準備以《Head First Design Patterns》的結構為主線(xiàn),所以每個(gè)模式的核心故事都是取材于此書(shū),在此再次聲明一下。不管怎樣,宗旨是為了跟大家一起循序漸進(jìn)地去認識設計模式。
上一篇:模式和原則,得到很多朋友的支持和鼓勵,這里再次深表感謝。這里我還是想呼吁一下,希望大家看過(guò)后多提寶貴意見(jiàn),反對意見(jiàn)更好,關(guān)鍵是我們在互動(dòng)中可以共同進(jìn)步,因為經(jīng)驗告訴我討論(爭論更甚)出來(lái)的火花,總是印象最深刻的。
其實(shí)策略模式是一個(gè)很簡(jiǎn)單的模式,也是一個(gè)很常用的模式,可謂短小精悍。我在介紹這個(gè)模式的同時(shí),為了加深大家對OO的理解,還會(huì )反復強調前面講過(guò)的設計原則和GRASP模式。這個(gè)系列的文章前后多少會(huì )有一些關(guān)聯(lián)的連續性,但是單獨一篇文章針對單一模式也一定是獨立的,所以不論大家想從前往后連續看也好,還是挑喜歡的跳著(zhù)看,都沒(méi)有問(wèn)題。
“羅嗦了這么多,太唐僧了吧,快點(diǎn)開(kāi)始吧…”(爛西紅柿和臭雞蛋從四面八方飛來(lái))
模擬鴨子
Joe是一名OO程序員,他為一家開(kāi)發(fā)模擬鴨子池塘游戲的公司工作,該公司的主要產(chǎn)品是一種可以模擬展示多種會(huì )游泳和呷呷叫的鴨子的游戲。這個(gè)游戲是使用標準的面向對象技術(shù)開(kāi)發(fā)的,系統里所有鴨子都繼承于Duck基類(lèi),系統的核心類(lèi)圖如下:

但是,商場(chǎng)如戰場(chǎng),不進(jìn)則退。Joe的公司最近的日子不好過(guò),盜版泛濫,再加上競爭對手的圍追堵劫,已經(jīng)拖欠好幾個(gè)月工資了。因此,公司高層在一次集體“腐敗”后,決定一定要給系統增加一些超玄的功能,以徹底擊垮競爭對手。經(jīng)過(guò)董事會(huì )討論,最終覺(jué)得如果能讓鴨子飛起來(lái),那么一定可以給對手致命一擊。于是Joe的上司對董事們拍著(zhù)胸脯說(shuō):“這沒(méi)有問(wèn)題,Joe是一個(gè)OO程序員,這對他來(lái)說(shuō)太簡(jiǎn)單了!我們保證一周內結束戰斗。”
接到任務(wù)的Joe絲毫不敢怠慢,研究了上級的指示以后,發(fā)現只要在Duck里增加一個(gè)fly()方法就可以搞定了,這樣所有繼承Duck的鴨子就都擁有了會(huì )飛的能力,哈!這回獎金有盼頭啦!改進(jìn)后的系統類(lèi)圖如下:
…… Joe的上司:“我正在給董事們演示你會(huì )飛的鴨子,但是怎么有很多橡皮鴨子也在四處亂飛呢?你在耍我嗎?你還想不想混啦?!”(此處省略粗話(huà)100字) Joe被嚇壞了,到手的獎金泡湯了!冷靜下來(lái)的Joe發(fā)現,原來(lái)在Duck類(lèi)里增加的方法,也同樣被繼承于Duck的RubberDuck類(lèi)繼承了,所以就有了會(huì )飛的橡皮鴨子,這是嚴重違反該系統“真實(shí)模擬各種鴨子”的原則的!那么該怎么辦呢?Joe很郁悶!他突然想到:如果在RubberDuck類(lèi)里把fly()方法重寫(xiě)一下會(huì )如何?在RubberDuck類(lèi)的fly()里讓橡皮鴨子什么都不做,不就一切OK了嗎!那以后再增加一個(gè)木頭鴨子呢?它不會(huì )飛也不會(huì )叫,那不是要再重寫(xiě)quack()和fly()方法,以后再增加其它特殊的鴨子都要這樣,這不是太麻煩了,而且也很混亂。 最終,Joe認識到使用繼承不是辦法,因為他的上司通知他,董事會(huì )決定以后每6個(gè)月就會(huì )升級一次系統,以應對市場(chǎng)競爭,所以未來(lái)的變化會(huì )很頻繁,而且還不可預知。如果以后靠逐個(gè)類(lèi)去判斷是否重寫(xiě)了quack()或fly()方法來(lái)應對變化,顯然混不下去! (Joe這時(shí)很迷惑,為什么屢試不爽的繼承,在系統維護升級的時(shí)候,無(wú)法很好地支持重用呢?) 那么使用接口怎么樣?我可以把fly()方法放在接口里,只有那些會(huì )飛的鴨子才需要實(shí)現這個(gè)接口,最好把quack()方法也拿出來(lái)放到一個(gè)接口里,因為有些鴨子是不會(huì )叫的。就像下面這樣: 呵呵!如果你是Joe,你該怎么辦? 我們知道,并不是所有的鴨子都會(huì )飛、會(huì )叫,所以繼承不是正確的方法。但是雖然上面的使用Flyable接口的方法,可以解決部分問(wèn)題(不再有會(huì )飛的橡皮鴨子),但是這個(gè)解決方案卻徹底破壞了重用,它帶來(lái)了另一個(gè)維護的噩夢(mèng)!而且還有一個(gè)問(wèn)題我們前面沒(méi)有提到,難道所有的鴨子的飛行方式、叫聲等行為都是一模一樣的嗎?不可能吧! 說(shuō)到這里,為了能幫助Joe擺脫困境,我們有必要先停下來(lái),重新回顧一些面向對象設計原則。請您告訴我:“什么東西是在軟件開(kāi)發(fā)過(guò)程中是恒定不變的?”,您想到了嗎?對,那就是變化本身,正所謂“計劃沒(méi)有變化快”,所以直面“變化這個(gè)事實(shí)”才是正道!Joe面對的問(wèn)題是,鴨子的行為在子類(lèi)里持續不斷地改變,所以讓所有的子類(lèi)都擁有基類(lèi)的行為是不適當的,而使用上面的接口的方式,又破壞了代碼重用?,F在就需要用到我們的第一個(gè)設計原則: Identify the aspects of your application that vary and separate them from what stays the same.(找到系統中變化的部分,將變化的部分同其它穩定的部分隔開(kāi)。) 換句話(huà)說(shuō)就是:“找到變化并且把它封裝起來(lái),稍后你就可以在不影響其它部分的情況下修改或擴展被封裝的變化部分。” 盡管這個(gè)概念很簡(jiǎn)單,但是它幾乎是所有設計模式的基礎,所有模式都提供了使系統里變化的部分獨立于其它部分的方法。 OK!現在我們已經(jīng)有了一條設計原則,那么Joe的問(wèn)題怎么辦呢?就鴨子的問(wèn)題來(lái)說(shuō),變化的部分就是子類(lèi)里的行為。所以我們要把這部分行為封裝起來(lái),省得它們老惹麻煩!從目前的情況看,就是fly()和quack()行為總是不老實(shí),而swim()行為是很穩定的,這個(gè)行為是可以使用繼承來(lái)實(shí)現代碼重用的,所以,我們需要做的就是把fly()和quack()行為從Duck基類(lèi)里隔離出來(lái)。我們需要創(chuàng )建兩組不同的行為,一組表示fly()行為,一組表示quack()行為。為什么是兩組而不是兩個(gè)呢?因為對于不同的子類(lèi)來(lái)說(shuō),fly()和quack()的表現形式都是不一樣的,有的鴨子嘎嘎叫,有的卻呷呷叫。有了這兩組行為,我們就可以組合出不同的鴨子,例如:我們可能想要實(shí)例化一個(gè)新的MallardDuck(野鴨)實(shí)例,并且給它初始化一個(gè)特殊類(lèi)型的飛行行為(野鴨飛行能力比較強)。那么,如果我們可以這樣,更進(jìn)一步,為什么我們不可以動(dòng)態(tài)地改變一個(gè)鴨子的行為呢?換句話(huà)說(shuō),我們將在Duck類(lèi)里包含行為設置方法,所以我們可以說(shuō)在運行時(shí)改變MallardDuck的飛行行為,這聽(tīng)起來(lái)更酷更靈活了!那么我們到底要怎么做呢?回答這個(gè)問(wèn)題,先要看一下我們的第二個(gè)設計原則: Program to an interface, not an implementation.(面向接口編程,而不要面向實(shí)現編程。) 嘿!對于這個(gè)原則,不論是耳朵還是眼睛,是不是都太熟悉了!“接口”這個(gè)詞已經(jīng)被賦予太多的含義,搞的大家一說(shuō)點(diǎn)兒屁事就滿(mǎn)嘴往外蹦“接口”。那么它到底是什么意思呢?我們這里說(shuō)的接口是一個(gè)抽象的概念,不局限于語(yǔ)言層面的接口(例如C#里的interface)。一個(gè)接口也可以是一個(gè)抽象類(lèi),或者一個(gè)基類(lèi)也可以看作是一種接口的表現形式,因為基類(lèi)變量可以用來(lái)引用其子類(lèi)。要點(diǎn)在于,我們在面向接口編程的時(shí)候,可以使用多態(tài),那么實(shí)際運行的代碼只依賴(lài)于具體的接口(interface,抽象類(lèi),基類(lèi)),而不管這些接口提供的功能是如何實(shí)現的,也就是說(shuō),接口將系統的不同部分隔離開(kāi)來(lái),同時(shí)又將它們連接在一起。我的神??!接口真是太偉大了!(爛西紅柿和臭雞蛋從四面八方飛來(lái)) OK!這回該徹底解決Joe的問(wèn)題了! 根據面向接口編程的設計原則,我們應該用接口來(lái)隔離鴨子問(wèn)題中變化的部分,也就是鴨子的不穩定的行為(fly()、quack())。我們要用一個(gè)FlyBehavior接口表示鴨子的飛行行為,這個(gè)接口可以有多種不同的實(shí)現方式,可以“橫”著(zhù)分,也可以“豎”著(zhù)分,管它呢!這樣做的好處就是我們將鴨子的行為實(shí)現在一組獨立的類(lèi)里,具體的鴨子是通過(guò)FlyBehavior這個(gè)接口來(lái)調用這個(gè)行為的,因為Duck只依賴(lài)FlyBehavior接口,所以不需要管FlyBehavior是如何被實(shí)現的。如下面的類(lèi)圖,FlyBehavior和QuackBehavior接口都有不同的實(shí)現方式! 第一步:我們要給Duck類(lèi)增加兩個(gè)接口類(lèi)型的實(shí)例變量,分別是flyBehavior和quackBehavior,它們其實(shí)就是新的設計里的“飛行”和“叫喚”行為。每個(gè)鴨子對象都將會(huì )使用各種方式來(lái)設置這些變量,以引用它們期望的運行時(shí)的特殊行為類(lèi)型(使用橫著(zhù)飛,吱吱叫,等等)。 第二步:我們還要把fly()和quack()方法從Duck類(lèi)里移除,因為我們已經(jīng)把這些行為移到FlyBehavior和QuackBehavior接口里了。我們將使用兩個(gè)相似的PerformFly()和PerformQuack()方法來(lái)替換fly()和qucak()方法,后面你會(huì )看到這兩個(gè)新方法是如何起作用的。 第三步:我們要考慮什么時(shí)候初始化flyBehavior和quackBehavior變量。最簡(jiǎn)單的辦法就是在Duck類(lèi)初始化的時(shí)候同時(shí)初始化他們。但是我們這里還有更好的辦法,就是提供兩個(gè)可以動(dòng)態(tài)設置變量值的方法SetFlyBehavior()和SetQuackBehavior(),那么就可以在運行時(shí)動(dòng)態(tài)改變鴨子的行為了。 下面是修改后的Duck類(lèi)圖: 
Joe的上司很高興,帶著(zhù)新產(chǎn)品給董事們演示去了…… 



最后大家再看看演示代碼,因為代碼比較多,就不貼出來(lái)了,大家可以下載后參考:

這就是策略模式
前面說(shuō)了那么多,現在終于到了正式介紹我們今天的主角的時(shí)候啦!此刻心情真是好激動(dòng)??!其實(shí)我們在前面就是使用Strategy模式幫Joe度過(guò)了難過(guò),真不知道他發(fā)了獎金后要怎么感謝我們啊。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.(策略模式定義了一系列的算法,并將每一個(gè)算法封裝起來(lái),而且使它們還可以相互替換。策略模式讓算法獨立于使用它的客戶(hù)而獨立變化。)
怎么樣,有了前面Joe的經(jīng)歷,這個(gè)定義理解起來(lái)還不那么太費勁吧?我想凡是認真看到這里的人,應該都能理解的。那么下面再畫(huà)蛇添足地羅嗦幾句,給那些還不太理解的朋友一個(gè)機會(huì )吧。J

l 需要使用ConcreteStrategy提供的算法。
l 內部維護一個(gè)Strategy的實(shí)例。
l 負責動(dòng)態(tài)設置運行時(shí)Strategy具體的實(shí)現算法。
l 負責跟Strategy之間的交互和數據傳遞。
Strategy(抽象策略類(lèi)):
l 定義了一個(gè)公共接口,各種不同的算法以不同的方式實(shí)現這個(gè)接口,Context使用這個(gè)接口調用不同的算法,一般使用接口或抽象類(lèi)實(shí)現。
ConcreteStrategy(具體策略類(lèi)):
l 實(shí)現了Strategy定義的接口,提供具體的算法實(shí)現。
還不理解?!我的神??!那再看看下面的順序圖吧,這是最后的機會(huì )啦!

應用場(chǎng)景和優(yōu)缺點(diǎn)
上面我們已經(jīng)看過(guò)了Strategy模式的詳細介紹,下面我們再來(lái)簡(jiǎn)單說(shuō)說(shuō)這個(gè)模式的優(yōu)缺點(diǎn)吧!怎么說(shuō)呢,人無(wú)完人,設計模式也不是萬(wàn)能的,每一個(gè)模式都有它的使命,也就是說(shuō)只有在特定的場(chǎng)景下才能發(fā)揮其功效。我們要使用好模式,就必須熟知各個(gè)模式的應用場(chǎng)景。
對于Strategy模式來(lái)說(shuō),主要有這些應用場(chǎng)景:
1、 多個(gè)類(lèi)只區別在表現行為不同,可以使用Strategy模式,在運行時(shí)動(dòng)態(tài)選擇具體要執行的行為。(例如FlyBehavior和QuackBehavior)
2、 需要在不同情況下使用不同的策略(算法),或者策略還可能在未來(lái)用其它方式來(lái)實(shí)現。(例如FlyBehavior和QuackBehavior的具體實(shí)現可任意變化或擴充)
3、 對客戶(hù)(Duck)隱藏具體策略(算法)的實(shí)現細節,彼此完全獨立。
對于Strategy模式來(lái)說(shuō),主要有如下優(yōu)點(diǎn):
1、 提供了一種替代繼承的方法,而且既保持了繼承的優(yōu)點(diǎn)(代碼重用)還比繼承更靈活(算法獨立,可以任意擴展)。
2、 避免程序中使用多重條件轉移語(yǔ)句,使系統更靈活,并易于擴展。
3、 遵守大部分GRASP原則和常用設計原則,高內聚、低偶合。
對于Strategy模式來(lái)說(shuō),主要有如下缺點(diǎn):
1、 因為每個(gè)具體策略類(lèi)都會(huì )產(chǎn)生一個(gè)新類(lèi),所以會(huì )增加系統需要維護的類(lèi)的數量。
備注:關(guān)于場(chǎng)景和優(yōu)缺點(diǎn),上面肯定說(shuō)得不夠全面,歡迎大家來(lái)補充。
.NET框架里的應用
Strategy模式的應用非常廣泛,也許大家有意無(wú)意之間一直都在使用。這里舉一個(gè).NET框架里使用Strategy模式的例子,象這樣的例子其實(shí)還有很多,只要大家細心體會(huì )就一定會(huì )發(fā)現的。
如果寫(xiě)過(guò)程序,那么ArrayList類(lèi)肯定都會(huì )用過(guò)吧,那么它的Sort方法想必大家也一定不陌生了。Sort方法的定義如下:
public virtual void Sort (IComparer comparer)
可以看到Sort方法接收一個(gè)IComparer類(lèi)型的參數,那么這個(gè)IComparer接口是做什么用的呢?下面我們看一段程序,下面的代碼示例演示如何使用默認比較器和一個(gè)反轉排序順序的自定義比較器,對 ArrayList 中的值進(jìn)行排序。(完全引自MSDN:ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.NETDEVFX.v20.chs/cpref2/html/M_System_Collections_ArrayList_Sort_1_a2d90598.htm)

2

3

4



5

6



7

8

9



10

11

12

13

14

15



16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46



47

48

49

50

51

52

53

54

55

56


57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

怎么樣,大家看出來(lái)了吧,其實(shí)在這段代碼里,ArrayList相當于Strategy模式中的Context(應用場(chǎng)景)部分,而IComparer相當于Strategy(抽象策略類(lèi))部分,myReverserClass相當于ConcreteStrategy(具體策略類(lèi))部分。我們這里拋開(kāi)myReverserClass類(lèi)的Compare方法如何具體實(shí)現不談,我們只要知道這是一個(gè)具體策略類(lèi),它提供了應用場(chǎng)景需要的具體算法,它實(shí)現了抽象策略類(lèi)接口,而應用場(chǎng)景通過(guò)抽象策略類(lèi)動(dòng)態(tài)調用到了具體策略類(lèi)中的算法。哈!所以這是一個(gè)十分典型的Strategy模式的應用。
基于這個(gè)符合Strategy模式的結構,我們還可以提供很多種自定義的具體策略類(lèi)的實(shí)現,只要這些類(lèi)實(shí)現了IComparer接口,就可以在運行時(shí)動(dòng)態(tài)設置給ArrayList類(lèi)的Sort方法,在Sort方法中會(huì )根據具體策略類(lèi)實(shí)現的比較算法規則來(lái)對ArrayList中的數據進(jìn)行排序。
最后一個(gè)設計原則
關(guān)于Strategy模式的故事講到這里,應該基本OK啦!下面我們再聊些更高層次的東西。什么是更高層次的東西?嘿!當然是設計原則了!在前面總結Strategy模式的優(yōu)點(diǎn)的時(shí)候我們提到過(guò),Strategy模式不僅保留了繼承的優(yōu)點(diǎn),而且還提供了更靈活的擴展能力。為什么會(huì )這樣呢?Strategy模式是怎么做到這一點(diǎn)的呢?哈!這是因為它“上面有人”??!誰(shuí)???它就是我們下面要介紹的重量級設計原則:
Favor composition over inheritance.(優(yōu)先使用對象組合,而非類(lèi)繼承)
關(guān)于組合和繼承,我們只要這樣來(lái)理解即可:組合是一種“HAS-A”關(guān)系,而繼承是一種“IS-A”關(guān)系。很明顯“HAS-A”要比“IS-A”更靈活一些。也就是說(shuō)在創(chuàng )建系統的時(shí)候,我們應該優(yōu)先使用對象組合,因為它不僅可以給你提供更多靈活性和擴展性,而且還使你可以在運行時(shí)改變行為(組合不同的對象),這簡(jiǎn)直是酷斃了!但是也不是說(shuō)繼承就是不能用,只是說(shuō)應該把繼承應用在相對更穩定,幾乎沒(méi)有變化的地方,例如前面的Duck類(lèi)里的Swim()方法,因為可以肯定所有鴨子一定都會(huì )游泳,所以就沒(méi)有必要給這個(gè)行為提供基于Strategy模式的實(shí)現方式,因為那樣做除了是程序更復雜以外,沒(méi)有什么意義。
BULLET POINTS
l Knowing the OO basics does not make you a good OO designer.
l Good OO designs are reusable,extensible and maintainable.
l Patterns show you how to build systems with good OO design qualities.
l Patterns are proven object oriented experience.
l Patterns don’t give you code,they give you general solutions to design problems.You apply them to your specific application.
l Patterns aren’t invented,they are discovered.
l Most patterns and principles address issues of change in software.
l Most patterns allow some part of a system to vary independently of all other parts.
l We often try to take what varies in a system and encapsulate it.
l Patterns provide a shared language that can maximize the value of your communication with other developers.
作者
王曉亮/Justin
MSN:xiaoliang203@hotmail.com
Mail:xiaoliang.justin@gmail.com
參考資料
《UML和模式應用》
《敏捷軟件開(kāi)發(fā)—原則、模式與實(shí)踐》
《Head First Design Patterns》
posted on 2007-02-06 00:42 Justin 閱讀(19997) 評論(90) 編輯 收藏 所屬分類(lèi): Design Patterns 、OOAD&UML

