《HeadFirst設計模式》第一章-策略模式

1.聲明(H1字號)

設計模式中的設計思想、圖片和部分代碼參考自《Head First設計模式》作者Eric Freeman & Elisabeth Freeman & Kathy Siezza & Bert Bates

在這裏我只是對這本書進行學習閱讀,並向大家分享一些心得體會。

2.開篇語

大學畢業已經九個月了,但是從事工作正好一年。自從學編程起,自己還沒有真正的開始進行設計模式的學習,以前總是在別人的框架中看到各種各樣的設計模式,但是並不知道開發者爲何這樣設計,有時我心裏總是在想,爲什麼別人要這麼設計,爲什麼要把事情搞得這麼麻煩呢?不光設計模式,就連Java中的接口,曾經也是讓我非常不理解它的用途,但是隨着學習的深入,逐漸發現大神們寫的框架設計的非常之巧妙,而這裏又涉及了大量的設計模式的知識,所以學習設計模式非常關鍵。程序員們總是自嘲碼農,但是誰也不想永遠當個碼農對吧,所以在我心裏,設計模式就是碼農通向軟件工程師中的重要一環。

我不覺得自己是個有毅力的人,因爲曾經我計劃過的很多事都未實現,現在開始學習設計模式,我希望自己能夠堅持下去,這篇就是我真正開始學習設計模式的第一篇-策略設計模式。

由於本人才疏學淺,在閱讀設計模式的時候有很多不懂的地方只能硬着頭皮去理解,自己的想法是先在大腦中貫徹出這種思想,然後再在實際工作中去慢慢體會,所以可能會有很多理解不周甚至錯誤的地方,歡迎各位指出來,我會及時進行修改。

3.策略設計模式

策略設計模式簡述:定義了算法族,分別封裝起來,讓它們之間可以相互替換,此模式讓算法的變化獨立於使用算法的客戶,算法族的替換不會影響到客戶端。

3.1需求(H2字號)

現在有一款遊戲要設計,遊戲中有各種各樣的鴨子,現在我們開始採用OO(面向對象)技術進行設計。

3.2開始設計

既然是面向對象設計方式,那麼不必多說,首先設計一個父類,然後設計兩個子類,分別是野鴨和紅頭鴨。

//鴨子的父類
public abstract class Duck {

	//所有的鴨子都會叫,所以這裏由父類實現了叫的方法
	public void quack() {
		System.out.println("duck call");
	}
	
	//所有的鴨子都會游泳,所以這裏由父類實現了游泳的方法
	public void swim() {
		System.out.println("duck swim");
	}
	
	//由於每個鴨子的外觀不同,所以設計爲抽象的由子類自行進行設計
	public abstract void display();
}
//野鴨子
public class MallardDuck extends Duck{

	@Override
	public void display() {
		System.out.println("野鴨子的外觀");
	}

}
//紅頭鴨子
public class RedheadDuck extends Duck{

	@Override
	public void display() {
		System.out.println("紅頭鴨子的外觀");
	}

}

 

3.3功能添加

現在遊戲主管決定對遊戲添加新的功能,決定讓鴨子飛起來,於此同時遊戲中添加了一個新的鴨子角色:橡皮鴨(經常在浴室出現的),於是一位OO程序員Joe,就在父類Duck中添加了一個fly()方法,那麼衆多鴨子就可以具有飛的功能了。

//鴨子的父類
public abstract class Duck {

	//所有的鴨子都會叫,所以這裏由父類實現了叫的方法
	public void quack() {
		System.out.println("duck call");
	}
	
	//所有的鴨子都會游泳,所以這裏由父類實現了游泳的方法
	public void swim() {
		System.out.println("duck swim");
	}
	
	//由於每個鴨子的外觀不同,所以設計爲抽象的由子類自行進行設計
	public abstract void display();
	
	public void fly() {
		System.out.println("鴨子在飛");
	}
}
//橡皮鴨
public class RubberDuck extends Duck{

	//假的橡皮鴨子不會呱呱叫,只能發出吱吱的聲音
	@Override
	public void quack() {
		System.out.println("覆蓋成吱吱叫");
	}

	@Override
	public void display() {
		System.out.println("外觀是橡皮鴨");
	}
	
}

但是這時公司的領導非常氣憤,說在遊戲中看見橡皮鴨子(假鴨子)飛是非常不合理的,於是Joe對橡皮鴨子進行如下更改。

//橡皮鴨
public class RubberDuck extends Duck{

	//假的橡皮鴨子不會呱呱叫,只能發出吱吱的聲音
	@Override
	public void quack() {
		System.out.println("覆蓋成吱吱叫");
	}

	@Override
	public void display() {
		System.out.println("外觀是橡皮鴨");
	}

	@Override
	public void fly() {
		//覆蓋改爲什麼也不做,爲了實現不讓橡皮鴨飛的目的
	}
	
}

但是這樣做並不能從根本解決問題,原因就是如果以後遊戲中添加了新的鴨子:木頭假鴨,那麼這種鴨子既不能叫,也不能飛,難不成將quack()和fly()都重寫一遍,而且還是僅僅都不做?這肯定是非常不合理的。

於是Joe又想到了新的辦法,他把fly()和quack()方法從父類中抽取出來,然後設計成了兩個接口,分別是Flyable接口和Quackable接口。

//飛行接口
public interface Flyable {

	//飛行方法
	public abstract void fly();
}
//叫接口
public interface Quackable {

	//叫方法
	public abstract void quack();
}

這樣具有飛行行爲和叫行爲的鴨子採取實現對應的接口,這樣看起來確實解決了強行覆蓋父類方法的問題,但是這樣卻有個更致命的問題,因爲我們知道接口中的方法沒有方法體,如果有48個鴨子,它們的飛行行爲大致形同,那麼我們就需要在每個子類中都實現一個方法48遍,而且很有可能在改需求的時候,也有可能改48遍,這樣的代碼毫無複用性可言,那麼這個問題到底該如何解決?

設計原則(H3字號)

找出應用中可能需要變化的部分,把他們獨立出來,不要和那些不需要變化的代碼混在一起。

換句話說,如果每次新的需求一來,都會使某方面的代碼發生變化,那麼這段代碼就需要抽取出來,和其他穩定的代碼有所區分。

3.4問題的解決思路

所以我們需要的是分開程序中變化的部分和不變的部分,其中變化的部分是fly()和quack(),那麼我們則需要將這兩個方法單獨拿出來,之前將這兩個方法設計成接口的思路是正確的,那麼我們只需要提高接口的複用性就可以了。那麼,我們可以設計如下兩個接口,而fly()和quack()行爲也從鴨子的類中單拿出來,將這兩個行爲設計爲新類,並實現對應的兩個接口。

那麼這樣鴨子的行爲就單獨設計成一個類,例如飛這個行爲,就有兩個實現類,分別是FlyWithWings和FlyNoWay。

那麼這樣設計,就可以讓這兩個行爲被其他對象所複用,因爲這兩個行爲已經與鴨子類無關了。

所以現在在設計鴨子類的時候,不需要在類的內部設計fly()和quack()兩個方法,而是在Duck中保留飛行行爲和叫行爲的兩個對象的引用,將飛行和叫委託給別人處理。 

設計原則

針對接口編程,而不是針對實現編程。

這裏的接口並非是java中狹義的接口(interface),而是廣義上的接口,即:超類型。

3.5代碼具體設計

//飛行行爲的接口
public interface FlyBehavior {

	//飛行方法
	public abstract void fly();
}

 

//具有飛行行爲的實現類
public class FlyWithWings implements FlyBehavior{

	@Override
	public void fly() {
		System.out.println("實現鴨子飛");
	}

}
//不具有飛行行爲的實現類
public class FlyNoWay implements FlyBehavior{
	@Override
	public void fly() {
		System.out.println("並不會飛");
	}
}
//呱呱叫行爲接口
public interface QuackBehavior {

	//呱呱叫方法
	public abstract void quack();
}
//具有呱呱叫的行爲的實現類
public class Quack implements QuackBehavior {
	public void quack() {
		System.out.println("實現鴨子呱呱叫");
	}
}
//具有吱吱叫(橡皮鴨子的叫的方式)的行爲的實現類
public class Squeak implements QuackBehavior {
	public void quack() {
		System.out.println("橡皮鴨子吱吱叫");
	}
}
//不會叫的實現類
public class MuteQuack implements QuackBehavior {
	public void quack() {
		System.out.println("並不會叫");
	}
}
//鴨子基類
public abstract class Duck {
	
	//具有飛行行爲的實現對象,鴨子飛行的委託者
	FlyBehavior flyBehavior;
	//具有叫行爲的實現對象,鴨子叫的委託者
	QuackBehavior quackBehavior;

	public Duck() {
	}

	//各個鴨子的外貌不同具體由子類實現
	abstract void display();

	//鴨子的飛行行爲委託給flyBehavior
	public void performFly() {
		flyBehavior.fly();
	}

	//鴨子的叫行爲委託給quackBehavior
	public void performQuack() {
		quackBehavior.quack();
	}

	public void swim() {
		System.out.println("所有的鴨子都會游泳");
	}
}

下面看看鴨子的某個具體實現類,例如野鴨的內部構造:

//野鴨
public class MallardDuck extends Duck {

	//構造方法中制定了具體的飛行委託者和叫委託者
	public MallardDuck() {
		//注:quackBehavior和flyBehavior子類可以直接訪問(只要和父類同包即可)
		quackBehavior = new Quack();
		flyBehavior = new FlyWithWings();
	}

	public void display() {
		System.out.println("我長得像一個野鴨子");
	}
}

下面書寫測試案例,來展現一下野鴨子的各種行爲:

//野鴨子的行爲展示案例
public class MiniDuckSimulator {
	 
	public static void main(String[] args) {
		//實例化野鴨子,MallardDuck在構造方法中對飛行和叫的委託者進行了初始化
		Duck mallard = new MallardDuck();
		
		//交給野鴨子的 叫委託者 展現叫行爲
		mallard.performQuack();
		//交給野鴨子的 飛行委託者 展現飛行行爲
		mallard.performFly();
		
		//使用的是父類的游泳方法
		mallard.swim();
		//使用的是自己本類中的外貌方法
		mallard.display();
	}
}

運行結果如下:

實現鴨子呱呱叫
實現鴨子飛
所有的鴨子都會游泳
我長得像一個野鴨子

3.6動態設定行爲

因爲在前文的設計原則中提到過,我們應該儘量的避免對具體實現進行編程,所以我們需要在Duck中提供對flyBehavior和quackBehavior做動態修改的方法,然後我們測試,動態設定行爲的好處。

修改Duck類,新增setFlyBehavior和setQuackBehavior這兩個方法:

//鴨子基類
public abstract class Duck {
	
	//具有飛行行爲的實現對象,鴨子飛行的委託者
	FlyBehavior flyBehavior;
	//具有叫行爲的實現對象,鴨子叫的委託者
	QuackBehavior quackBehavior;

	public Duck() {
	}

	//設置具體的飛行委託者,可以在程序運行時,指定特定的飛行委託者
	public void setFlyBehavior(FlyBehavior fb) {
		flyBehavior = fb;
	}

	//設置具體的叫委託者,可以在程序運行時,指定特定的叫委託者
	public void setQuackBehavior(QuackBehavior qb) {
		quackBehavior = qb;
	}

	//各個鴨子的外貌不同具體由子類實現
	abstract void display();

	//鴨子的飛行行爲委託給flyBehavior
	public void performFly() {
		flyBehavior.fly();
	}

	//鴨子的叫行爲委託給quackBehavior
	public void performQuack() {
		quackBehavior.quack();
	}

	public void swim() {
		System.out.println("所有的鴨子都會游泳");
	}
}

新增一個模型鴨類型:

//模型鴨子
public class ModelDuck extends Duck {
	
	//初始化時
	public ModelDuck() {
		//飛行行爲: 不會飛
		flyBehavior = new FlyNoWay();
		//叫行爲: 呱呱叫
		quackBehavior = new Quack(); 
	}

	public void display() {
		System.out.println("我的外貌是一個模型鴨");
	}
}

新增一個飛行行爲,火箭帶你飛:

//藉助火箭飛行的飛行行爲
public class FlyRocketPowered implements FlyBehavior {
	public void fly() {
		System.out.println("火箭帶我飛");
	}
}

下面我們修改一下測試類,看看這個動態行爲的設定究竟有什麼好處:

//野鴨子的行爲展示案例
public class MiniDuckSimulator {
	 
	public static void main(String[] args) {
		System.out.println("動態設定行爲之前,我是一個這樣的鴨子!!!");
		//實例化模型鴨
		Duck model = new ModelDuck();
		
		//展現自己的飛行行爲
		model.performFly();
		
		System.out.println();
		System.out.println("動態設定行爲之後,我逆襲成了這樣的鴨子!!!");
		//動態設置flyBehavior
		//FlyRocketPowered覆蓋掉了原ModelDuck構造器中的FlyNoWay
		model.setFlyBehavior(new FlyRocketPowered());
		//展現自己的飛行行爲
		model.performFly();
	}
}

運行結果:

動態設定行爲之前,我是一個這樣的鴨子!!!
並不會飛

動態設定行爲之後,我逆襲成了這樣的鴨子!!!
火箭帶我飛

經過了動態的行爲設定,這個模型鴨成功的飛上了天。

3.7類圖總覽

現在我們站在高處俯瞰全局,則會更加的清晰明瞭:

我們可以把不同的行爲看成不同的算法族,以Fly爲例,我們可以根據具體需要來選擇傳遞的FlyBehavior到底是FlyNoWay還是FlyWithWings,根據具體的需求選擇具體的算法。

3.8多用組合

我們發現,Duck中既包含FlyBehavior又包含QuackBehavior,所以我們可以認爲鴨子的行爲是由這些行爲模塊組合而成的,相比於繼承而言,組合的好處是:使系統具有很大的彈性,不僅可將算法族封裝成類,更可以在運行時動態的改變行爲,只要組合的對象符合正確的接口標準即可。

4.總結

策略模式思想總結:其思想是針對一組算法,將每一種算法都封裝到具有共同接口的獨立的類中,從而是它們可以相互替換。策略模式的最大特點是使得算法可以在不影響客戶端的情況下發生變化,從而改變不同的功能。

策略模式的使用案例還可參考:http://baijiahao.baidu.com/s?id=1601547440739500969&wfr=spider&for=pc

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