閱讀436 返回首頁    go 技術社區[雲棲]


設計模式之一:策略模式(Strategy pattern)

設計模式之一:策略模式(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、  因為每個具體策略類都會產生一個新類,所以會增加係統需要維護的類的數量。

封裝行為的大局觀

我們已經深入研究了鴨子模擬器的設計,概述將頭探出水麵,唿吸空氣的時候了,先來就來看看整體的格局。
下麵是整個重新設計後的類結構,你所期望的一切都有:鴨子繼承Duck,飛行行為實現FlyBehavior接口,呱呱叫行為實現QuackBehavior接口。類圖如下圖所示:

最後一個設計原則:多用組合,少用繼承。
將兩個類結合起來使用,如同本例一般,這就是組合。這種紅作法和“集成”不同的地方在於,鴨子的行為不是繼承來的,而是和適當的行為對象“組合”來的。使用組合建立係統具有很大的彈性,不僅可將算法族封裝成類,更可以“在運行時動態地改變行為”,隻要組合的行為對象符合正確的接口標準即可。


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

  上一篇:go Android Dialog用法
  下一篇:go mac os X下開啟root用戶