萬事開頭難,最近對這句話體會深入!這篇文章是這個系列正式開始介紹設計模式的第一篇,因此肩負着肯定這個系列風格的歷史重任,它在我腦殼裏默默地醞釀了好多天,卻只搜刮出了一點兒不太清晰的輪廓,但是時間不等人,之後再多「迭代」幾回吧!在前面的隨筆裏,我已經提到了,這個系列準備以《Head First Design Patterns》的結構爲主線,因此每一個模式的核心故事都是取材於此書,在此再次聲明一下。無論怎樣,宗旨是爲了跟你們一塊兒按部就班地去認識設計模式。 html
上一篇:模式和原則,獲得不少朋友的支持和鼓勵,這裏再次深表感謝。這裏我仍是想呼籲一下,但願你們看事後多提寶貴意見,反對意見更好,關鍵是咱們在互動中能夠共同進步,由於經驗告訴我討論(爭論更甚)出來的火花,老是印象最深入的。 程序員
其實策略模式是一個很簡單的模式,也是一個很經常使用的模式,可謂短小精悍。我在介紹這個模式的同時,爲了加深你們對OO的理解,還會反覆強調前面講過的設計原則和GRASP模式。這個系列的文章先後多少會有一些關聯的連續性,可是單獨一篇文章針對單一模式也必定是獨立的,因此不論你們想從前日後連續看也好,仍是挑喜歡的跳着看,都沒有問題。 算法
「羅嗦了這麼多,太唐僧了吧,快點開始吧…」(爛西紅柿和臭雞蛋從四面八方飛來) 編程
Joe是一名OO程序員,他爲一家開發模擬鴨子池塘遊戲的公司工做,該公司的主要產品是一種能夠模擬展現多種會游泳和呷呷叫的鴨子的遊戲。這個遊戲是使用標準的面向對象技術開發的,系統裏全部鴨子都繼承於Duck基類,系統的核心類圖以下: 設計模式
可是,商場如戰場,不進則退。Joe的公司最近的日子很差過,盜版氾濫,再加上競爭對手的圍追堵劫,已經拖欠好幾個月工資了。所以,公司高層在一次集體「腐敗」後,決定必定要給系統增長一些超玄的功能,以完全擊垮競爭對手。通過董事會討論,最終以爲若是能讓鴨子飛起來,那麼必定能夠給對手致命一擊。因而Joe的上司對董事們拍着胸脯說:「這沒有問題,Joe是一個OO程序員,這對他來講太簡單了!咱們保證一週內結束戰鬥。」 app
接到任務的Joe絲絕不敢怠慢,研究了上級的指示之後,發現只要在Duck裏增長一個fly()方法就能夠搞定了,這樣全部繼承Duck的鴨子就都擁有了會飛的能力,哈!這回獎金有盼頭啦!改進後的系統類圖以下: 框架
…… ui
Joe的上司:「我正在給董事們演示你會飛的鴨子,可是怎麼有不少橡皮鴨子也在四處亂飛呢?你在耍我嗎?你還想不想混啦?!」(此處省略粗話100字) spa
Joe被嚇壞了,到手的獎金泡湯了!冷靜下來的Joe發現,原來在Duck類裏增長的方法,也一樣被繼承於Duck的RubberDuck類繼承了,因此就有了會飛的橡皮鴨子,這是嚴重違反該系統「真實模擬各類鴨子」的原則的!那麼該怎麼辦呢?Joe很鬱悶!他忽然想到:若是在RubberDuck類裏把fly()方法重寫一下會如何?在RubberDuck類的fly()裏讓橡皮鴨子什麼都不作,不就一切OK了嗎!那之後再增長一個木頭鴨子呢?它不會飛也不會叫,那不是要再重寫quack()和fly()方法,之後再增長其它特殊的鴨子都要這樣,這不是太麻煩了,並且也很混亂。 .net
最終,Joe認識到使用繼承不是辦法,由於他的上司通知他,董事會決定之後每6個月就會升級一次系統,以應對市場競爭,因此將來的變化會很頻繁,並且還不可預知。若是之後靠逐個類去判斷是否重寫了quack()或fly()方法來應對變化,顯然混不下去!
(Joe這時很迷惑,爲何屢試不爽的繼承,在系統維護升級的時候,沒法很好地支持重用呢?)
那麼使用接口怎麼樣?我能夠把fly()方法放在接口裏,只有那些會飛的鴨子才須要實現這個接口,最好把quack()方法也拿出來放到一個接口裏,由於有些鴨子是不會叫的。就像下面這樣:
呵呵!若是你是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接口都有不一樣的實現方式!
第一步:咱們要給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模式來講,主要有這些應用場景:
一、 多個類只區別在表現行爲不一樣,可使用Strategy模式,在運行時動態選擇具體要執行的行爲。(例如FlyBehavior和QuackBehavior)
二、 須要在不一樣狀況下使用不一樣的策略(算法),或者策略還可能在將來用其它方式來實現。(例如FlyBehavior和QuackBehavior的具體實現可任意變化或擴充)
三、 對客戶(Duck)隱藏具體策略(算法)的實現細節,彼此徹底獨立。
對於Strategy模式來講,主要有以下優勢:
一、 提供了一種替代繼承的方法,並且既保持了繼承的優勢(代碼重用)還比繼承更靈活(算法獨立,能夠任意擴展)。
二、 避免程序中使用多重條件轉移語句,使系統更靈活,並易於擴展。
三、 遵照大部分GRASP原則和經常使用設計原則,高內聚、低偶合。
對於Strategy模式來講,主要有以下缺點:
一、 由於每一個具體策略類都會產生一個新類,因此會增長系統須要維護的類的數量。
備註:關於場景和優缺點,上面確定說得不夠全面,歡迎你們來補充。
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)
怎麼樣,你們看出來了吧,其實在這段代碼裏,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模式的實現方式,由於那樣作除了是程序更復雜之外,沒有什麼意義。