策略模式(Strategy
)
前言
萬事開頭難,最近對這句話體會深刻!這篇文章是這個系列正式開始介紹設(shè)計模式的第一篇,所以肩負(fù)著確定這個系列風(fēng)格的歷史重任,它在我腦袋里默默地醞釀了好多天,卻只搜刮出了一點兒不太清晰的輪廓,可是時間不等人,以后再多“迭代”幾次吧!在前面的隨筆里,我已經(jīng)提到了,這個系列準(zhǔn)備以《Head First Design Patterns》的結(jié)構(gòu)為主線,所以每個模式的核心故事都是取材于此書,在此再次聲明一下。不管怎樣,宗旨是為了跟大家一起循序漸進(jìn)地去認(rèn)識設(shè)計模式。
上一篇:模式和原則,得到很多朋友的支持和鼓勵,這里再次深表感謝。這里我還是想呼吁一下,希望大家看過后多提寶貴意見,反對意見更好,關(guān)鍵是我們在互動中可以共同進(jìn)步,因為經(jīng)驗告訴我討論(爭論更甚)出來的火花,總是印象最深刻的。
其實策略模式是一個很簡單的模式,也是一個很常用的模式,可謂短小精悍。我在介紹這個模式的同時,為了加深大家對OO的理解,還會反復(fù)強(qiáng)調(diào)前面講過的設(shè)計原則和GRASP模式。這個系列的文章前后多少會有一些關(guān)聯(lián)的連續(xù)性,但是單獨一篇文章針對單一模式也一定是獨立的,所以不論大家想從前往后連續(xù)看也好,還是挑喜歡的跳著看,都沒有問題。
“羅嗦了這么多,太唐僧了吧,快點開始吧…”(爛西紅柿和臭雞蛋從四面八方飛來)
模擬鴨子
Joe是一名OO程序員,他為一家開發(fā)模擬鴨子池塘游戲的公司工作,該公司的主要產(chǎn)品是一種可以模擬展示多種會游泳和呷呷叫的鴨子的游戲。這個游戲是使用標(biāo)準(zhǔn)的面向?qū)ο蠹夹g(shù)開發(fā)的,系統(tǒng)里所有鴨子都繼承于Duck基類,系統(tǒng)的核心類圖如下:
如圖所示,在Duck基類里實現(xiàn)了公共的quack()和swim()方法,而MallardDuck和RedheadDuck可以分別覆蓋實現(xiàn)自己的display()方法,這樣即重用了公共的部分,又支持不同子類的個性化擴(kuò)展。從目前的情況看,這是一個很好的設(shè)計,哈!
但是,商場如戰(zhàn)場,不進(jìn)則退。Joe的公司最近的日子不好過,盜版泛濫,再加上競爭對手的圍追堵劫,已經(jīng)拖欠好幾個月工資了。因此,公司高層在一次集體“腐敗”后,決定一定要給系統(tǒng)增加一些超玄的功能,以徹底擊垮競爭對手。經(jīng)過董事會討論,最終覺得如果能讓鴨子飛起來,那么一定可以給對手致命一擊。于是Joe的上司對董事們拍著胸脯說:“這沒有問題,Joe是一個OO程序員,這對他來說太簡單了!我們保證一周內(nèi)結(jié)束戰(zhàn)斗。”
接到任務(wù)的Joe絲毫不敢怠慢,研究了上級的指示以后,發(fā)現(xiàn)只要在Duck里增加一個fly()方法就可以搞定了,這樣所有繼承Duck的鴨子就都擁有了會飛的能力,哈!這回獎金有盼頭啦!改進(jìn)后的系統(tǒng)類圖如下:
Joe
的上司很高興,帶著新產(chǎn)品給董事們演示去了……
……
Joe的上司:“我正在給董事們演示你會飛的鴨子,但是怎么有很多橡皮鴨子也在四處亂飛呢?你在耍我嗎?你還想不想混啦?!”(此處省略粗話100字)
Joe被嚇壞了,到手的獎金泡湯了!冷靜下來的Joe發(fā)現(xiàn),原來在Duck類里增加的方法,也同樣被繼承于Duck的RubberDuck類繼承了,所以就有了會飛的橡皮鴨子,這是嚴(yán)重違反該系統(tǒng)“真實模擬各種鴨子”的原則的!那么該怎么辦呢?Joe很郁悶!他突然想到:如果在RubberDuck類里把fly()方法重寫一下會如何?在RubberDuck類的fly()里讓橡皮鴨子什么都不做,不就一切OK了嗎!那以后再增加一個木頭鴨子呢?它不會飛也不會叫,那不是要再重寫quack()和fly()方法,以后再增加其它特殊的鴨子都要這樣,這不是太麻煩了,而且也很混亂。
最終,Joe認(rèn)識到使用繼承不是辦法,因為他的上司通知他,董事會決定以后每6個月就會升級一次系統(tǒng),以應(yīng)對市場競爭,所以未來的變化會很頻繁,而且還不可預(yù)知。如果以后靠逐個類去判斷是否重寫了quack()或fly()方法來應(yīng)對變化,顯然混不下去!
(Joe這時很迷惑,為什么屢試不爽的繼承,在系統(tǒng)維護(hù)升級的時候,無法很好地支持重用呢?)
那么使用接口怎么樣?我可以把fly()方法放在接口里,只有那些會飛的鴨子才需要實現(xiàn)這個接口,最好把quack()方法也拿出來放到一個接口里,因為有些鴨子是不會叫的。就像下面這樣:
Joe的上司知道后怒了:“你這樣做難道是希望所有需要quack()和fly()方法的鴨子都去重復(fù)實現(xiàn)這兩個方法的功能嗎?就這么幾個鴨子還好說,但是我們有幾十、上百個鴨子的時候你怎么辦?如果某個方法要做一點修改,難道你要重復(fù)修改上百遍嗎?你是不是瘋啦?”
呵呵!如果你是Joe,你該怎么辦?
我們知道,并不是所有的鴨子都會飛、會叫,所以繼承不是正確的方法。但是雖然上面的使用Flyable接口的方法,可以解決部分問題(不再有會飛的橡皮鴨子),但是這個解決方案卻徹底破壞了重用,它帶來了另一個維護(hù)的噩夢!而且還有一個問題我們前面沒有提到,難道所有的鴨子的飛行方式、叫聲等行為都是一模一樣的嗎?不可能吧!
說到這里,為了能幫助Joe擺脫困境,我們有必要先停下來,重新回顧一些面向?qū)ο笤O(shè)計原則。請您告訴我:“什么東西是在軟件開發(fā)過程中是恒定不變的?”,您想到了嗎?對,那就是變化本身,正所謂“計劃沒有變化快”,所以直面“變化這個事實”才是正道!Joe面對的問題是,鴨子的行為在子類里持續(xù)不斷地改變,所以讓所有的子類都擁有基類的行為是不適當(dāng)?shù)模褂蒙厦娴慕涌诘姆绞剑制茐牧舜a重用。現(xiàn)在就需要用到我們的第一個設(shè)計原則:
Identify the aspects of your application that vary and separate them from what stays the same.(找到系統(tǒng)中變化的部分,將變化的部分同其它穩(wěn)定的部分隔開。)
換句話說就是:“找到變化并且把它封裝起來,稍后你就可以在不影響其它部分的情況下修改或擴(kuò)展被封裝的變化部分。” 盡管這個概念很簡單,但是它幾乎是所有設(shè)計模式的基礎(chǔ),所有模式都提供了使系統(tǒng)里變化的部分獨立于其它部分的方法。
OK!現(xiàn)在我們已經(jīng)有了一條設(shè)計原則,那么Joe的問題怎么辦呢?就鴨子的問題來說,變化的部分就是子類里的行為。所以我們要把這部分行為封裝起來,省得它們老惹麻煩!從目前的情況看,就是fly()和quack()行為總是不老實,而swim()行為是很穩(wěn)定的,這個行為是可以使用繼承來實現(xiàn)代碼重用的,所以,我們需要做的就是把fly()和quack()行為從Duck基類里隔離出來。我們需要創(chuàng)建兩組不同的行為,一組表示fly()行為,一組表示quack()行為。為什么是兩組而不是兩個呢?因為對于不同的子類來說,fly()和quack()的表現(xiàn)形式都是不一樣的,有的鴨子嘎嘎叫,有的卻呷呷叫。有了這兩組行為,我們就可以組合出不同的鴨子,例如:我們可能想要實例化一個新的MallardDuck(野鴨)實例,并且給它初始化一個特殊類型的飛行行為(野鴨飛行能力比較強(qiáng))。那么,如果我們可以這樣,更進(jìn)一步,為什么我們不可以動態(tài)地改變一個鴨子的行為呢?換句話說,我們將在Duck類里包含行為設(shè)置方法,所以我們可以說在運行時改變MallardDuck的飛行行為,這聽起來更酷更靈活了!那么我們到底要怎么做呢?回答這個問題,先要看一下我們的第二個設(shè)計原則:
Program to an interface, not an implementation.(面向接口編程,而不要面向?qū)崿F(xiàn)編程。)
嘿!對于這個原則,不論是耳朵還是眼睛,是不是都太熟悉了!“接口”這個詞已經(jīng)被賦予太多的含義,搞的大家一說點兒屁事就滿嘴往外蹦“接口”。那么它到底是什么意思呢?我們這里說的接口是一個抽象的概念,不局限于語言層面的接口(例如C#里的interface)。一個接口也可以是一個抽象類,或者一個基類也可以看作是一種接口的表現(xiàn)形式,因為基類變量可以用來引用其子類。要點在于,我們在面向接口編程的時候,可以使用多態(tài),那么實際運行的代碼只依賴于具體的接口(interface,抽象類,基類),而不管這些接口提供的功能是如何實現(xiàn)的,也就是說,接口將系統(tǒng)的不同部分隔離開來,同時又將它們連接在一起。我的神啊!接口真是太偉大了!(爛西紅柿和臭雞蛋從四面八方飛來)
OK!這回該徹底解決Joe的問題了!
根據(jù)面向接口編程的設(shè)計原則,我們應(yīng)該用接口來隔離鴨子問題中變化的部分,也就是鴨子的不穩(wěn)定的行為(fly()、quack())。我們要用一個FlyBehavior接口表示鴨子的飛行行為,這個接口可以有多種不同的實現(xiàn)方式,可以“橫”著分,也可以“豎”著分,管它呢!這樣做的好處就是我們將鴨子的行為實現(xiàn)在一組獨立的類里,具體的鴨子是通過FlyBehavior這個接口來調(diào)用這個行為的,因為Duck只依賴FlyBehavior接口,所以不需要管FlyBehavior是如何被實現(xiàn)的。如下面的類圖,FlyBehavior和QuackBehavior接口都有不同的實現(xiàn)方式!
Joe已經(jīng)暈了,“你說了這么多,全是大白話,來點代碼行不行,我要C#的!”。說到這里,我們也該開始徹底改造這個設(shè)計了,并會在最后附加部分代碼來幫助大家理解。
第一步:我們要給Duck類增加兩個接口類型的實例變量,分別是flyBehavior和quackBehavior,它們其實就是新的設(shè)計里的“飛行”和“叫喚”行為。每個鴨子對象都將會使用各種方式來設(shè)置這些變量,以引用它們期望的運行時的特殊行為類型(使用橫著飛,吱吱叫,等等)。
第二步:我們還要把fly()和quack()方法從Duck類里移除,因為我們已經(jīng)把這些行為移到FlyBehavior和QuackBehavior接口里了。我們將使用兩個相似的PerformFly()和PerformQuack()方法來替換fly()和qucak()方法,后面你會看到這兩個新方法是如何起作用的。
第三步:我們要考慮什么時候初始化flyBehavior和quackBehavior變量。最簡單的辦法就是在Duck類初始化的時候同時初始化他們。但是我們這里還有更好的辦法,就是提供兩個可以動態(tài)設(shè)置變量值的方法SetFlyBehavior()和SetQuackBehavior(),那么就可以在運行時動態(tài)改變鴨子的行為了。
下面是修改后的Duck類圖:
我們再看看整個設(shè)計修改后的類圖:
最后大家再看看演示代碼,因為代碼比較多,就不貼出來了,大家可以下載后參考:
。下面是演示代碼的執(zhí)行結(jié)果:
這就是策略模式
前面說了那么多,現(xiàn)在終于到了正式介紹我們今天的主角的時候啦!此刻心情真是好激動啊!其實我們在前面就是使用Strategy模式幫Joe度過了難過,真不知道他發(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.(策略模式定義了一系列的算法,并將每一個算法封裝起來,而且使它們還可以相互替換。策略模式讓算法獨立于使用它的客戶而獨立變化。)
怎么樣,有了前面Joe的經(jīng)歷,這個定義理解起來還不那么太費勁吧?我想凡是認(rèn)真看到這里的人,應(yīng)該都能理解的。那么下面再畫蛇添足地羅嗦幾句,給那些還不太理解的朋友一個機(jī)會吧。J
Context(應(yīng)用場景):
l 需要使用ConcreteStrategy提供的算法。
l 內(nèi)部維護(hù)一個Strategy的實例。
l 負(fù)責(zé)動態(tài)設(shè)置運行時Strategy具體的實現(xiàn)算法。
l 負(fù)責(zé)跟Strategy之間的交互和數(shù)據(jù)傳遞。
Strategy(抽象策略類):
l 定義了一個公共接口,各種不同的算法以不同的方式實現(xiàn)這個接口,Context使用這個接口調(diào)用不同的算法,一般使用接口或抽象類實現(xiàn)。
ConcreteStrategy(具體策略類):
l 實現(xiàn)了Strategy定義的接口,提供具體的算法實現(xiàn)。
還不理解?!我的神啊!那再看看下面的順序圖吧,這是最后的機(jī)會啦!
應(yīng)用場景和優(yōu)缺點
上面我們已經(jīng)看過了Strategy模式的詳細(xì)介紹,下面我們再來簡單說說這個模式的優(yōu)缺點吧!怎么說呢,人無完人,設(shè)計模式也不是萬能的,每一個模式都有它的使命,也就是說只有在特定的場景下才能發(fā)揮其功效。我們要使用好模式,就必須熟知各個模式的應(yīng)用場景。
對于Strategy模式來說,主要有這些應(yīng)用場景:
1、 多個類只區(qū)別在表現(xiàn)行為不同,可以使用Strategy模式,在運行時動態(tài)選擇具體要執(zhí)行的行為。(例如FlyBehavior和QuackBehavior)
2、 需要在不同情況下使用不同的策略(算法),或者策略還可能在未來用其它方式來實現(xiàn)。(例如FlyBehavior和QuackBehavior的具體實現(xiàn)可任意變化或擴(kuò)充)
3、 對客戶(Duck)隱藏具體策略(算法)的實現(xiàn)細(xì)節(jié),彼此完全獨立。
對于Strategy模式來說,主要有如下優(yōu)點:
1、 提供了一種替代繼承的方法,而且既保持了繼承的優(yōu)點(代碼重用)還比繼承更靈活(算法獨立,可以任意擴(kuò)展)。
2、 避免程序中使用多重條件轉(zhuǎn)移語句,使系統(tǒng)更靈活,并易于擴(kuò)展。
3、 遵守大部分GRASP原則和常用設(shè)計原則,高內(nèi)聚、低偶合。
對于Strategy模式來說,主要有如下缺點:
1、 因為每個具體策略類都會產(chǎn)生一個新類,所以會增加系統(tǒng)需要維護(hù)的類的數(shù)量。
備注:關(guān)于場景和優(yōu)缺點,上面肯定說得不夠全面,歡迎大家來補(bǔ)充。