有些人已經解決你的問題了
什麼是設計模式?我們為什麼要使用設計模式?怎樣使用?按照書上的說法和我自己的理解,我認為是這樣的:我們遇到的問題其他開發人員也遇到過,他們利用他們的智慧和經驗將問題解決了,把他們解決問題的方法提煉總結出來使其能解決其他同類問題。使用設計模式是為了更方便快捷的解決問題。把模式裝進腦子裡,然後在你的設計和已有的應用中,尋找何處可以使用這些模式,以往是代碼復用,現在是經驗復用。
先把書上的例子過一遍,簡單的鴨子模擬應用
Joe所在的公司決定開發一款模擬鴨子的應用。系統中有各種鴨子,鴨子可以游泳,可以呱呱叫,各種鴨子有不同的外觀。此系統建立了一個鴨子的基類,其中有游泳的方法swim,有呱呱叫的方法quack,有顯示鴨子外貌的方法display。每種鴨子的外貌不同,必須在其子類中重寫display方法。就像這樣:
由於競爭加劇,公司決定搞定不一樣的東西:“嘿,Joe,我想鴨子應該能飛!”,“嗯, 這個聽起來很容易”,在Duck中加一個fly方法就行了。過了幾天,公司開會,“Joe,怎麼會有一只橡皮鴨(RubberDuck,不會飛,吱吱叫)在屏幕裡面飛來飛去?”,好吧,這是Joe疏忽了,只有真正的鴨子能飛,橡皮鴨會叫,會游泳但是不會飛,馬上修復(覆蓋RubberDuck中的fly方法, 讓它什麼也不做)。但是如果我需要一只誘餌鴨(DecoyDuck,不會飛也不會叫)呢,也在子類中重寫quack和fly方法?Joe還收到通知,此系統還會不定時更新,至於怎麼更新還沒有想到,於是Joe意識到繼承不是一個好方法,因為每添加一個鴨子的子類,他就要被迫檢查該子類的quack和fly方法並可能需要重寫他們,如果直接修改父類中的方法,但有些子類並不想修改,那麼這些子類就都要重寫這些方法。
繼承所采用的代碼
Duck
public abstract class Duck { public void quack(){ System.out.println("呱呱叫"); } public void swim(){ System.out.println("游泳"); } //每個鴨子的外觀不同, 在子類中實現 public abstract void display(); //鴨子飛行的方法 public void fly(){ System.out.println("飛行"); } }View Code
MallardDuck
/** * 外觀是綠色的鴨子 */ public class MallardDuck extends Duck { @Override public void display() { System.out.println("綠頭鴨"); } }View Code
RubberDuck
/** * 橡皮鴨 * 橡皮鴨不會呱呱叫(quack), 而是吱吱叫(squeak) */ public class RubberDuck extends Duck { @Override public void display() { System.out.println("可愛的黃色橡皮鴨"); } //橡皮鴨不會呱呱叫(quack), 而是吱吱叫(squeak) @Override public void quack() { System.out.println("橡皮鴨吱吱叫"); } //橡皮鴨不會飛 @Override public void fly() { } }View Code
DecoyDuck
/** * 誘餌鴨, 不會飛也不會叫 */ public class DecoyDuck extends Duck { @Override public void display() { System.out.println("誘餌鴨"); } @Override public void quack() { System.out.println("什麼都不會做, 不會叫"); } @Override public void fly() { System.out.println("什麼都不做, 不會飛"); } }View Code
采用接口呢
將行為抽離出來,比如將fly方法抽離出來放到Flyable接口中,只有會飛的Duck的子類才實現該接口,同樣的也可以將quack方法抽離到Quackable接口中。就像這樣:
這樣解決了一部分問題,至少橡皮鴨不會到處飛了。但是你有沒有想過另外的一個問題,就是這樣代碼無法復用,因為接口中的方法只能是抽象的,這樣導致在每個子類中都需要重寫方法。這樣無疑是從一個噩夢跳入了另一個噩夢。
變化與不變分離
好吧,在軟件開發上,有什麼是你深信不疑的,那就是——change,不變的是變化。
雖然使用接口也不妥,但思想是值得借鑒的: 把變化的和不變的離,那哪些是變化的呢?“真正的”鴨子會游泳swim,會呱呱叫quack,會飛fly,而橡皮鴨會游泳swim,會吱吱叫squeak,不會飛。所以說叫可以是呱呱叫,可以是吱吱叫,也可以不叫,飛可以沿直線飛,可以沿曲線飛,也可以不會飛,所以叫和飛是變化的。游泳都是在水面上無規則的游泳,可以看作是不變的,當然,你要認為游泳有多種方式,也可以認為它是變化的,我在這裡把游泳認為是不變的,所以說判斷變化的和不變的要根據實際情況來定。
軟件設計原則: 變化與不變分離
找出應用中可能會變化的部分,把它們獨立出來,不要把他們和那些不變的混在一起。即把應用中可能會變化的抽離出來並封裝起來。
繼續設計
基於變化和不變分離的原則,我們可以把變化的(飛和叫)抽離出來並封裝起來。比如將飛行的行為抽離出來設計成一個接口FlyBehavior,將叫的行為設計成接口QuackBehavior,然後具體的行為實現接口並重寫方法,如:
針對接口編程
我們把鴨子的行為從鴨子類Duck中分離出來,和以往不同,以往的做法是:行為是從Duck中的具體實現繼承過來,或是實現接口重寫方法而來,這兩種方式都依賴實現,我們被實現綁得死死的。在我們新的設計中,我們把行為分離出來,所以行為不會綁死在鴨子的子類中,換句話說現實行為的代碼位於特定類QuackBehavior和FlyBehavior中,可以在運行時動態的改變行為。這就是針對接口編程的一種體現,針對接口編程指的是針對超類型編程,不一定是interface,可以是abstract class。
軟件設計原則:針對接口編程
針對接口編程,而不是針對實現編程
整合實現代碼
首先是兩個行為接口QuackBehavior和FlyBehavior
QuackBehavior
public interface QuackBehavior { void quack(); }View Code
FlyBehavior
/** * 飛行行為接口 */ public interface FlyBehavior { void fly(); }View Code
QuackBehavior具體行為實現,呱呱叫Quack,吱吱叫Squeak,不會叫MuteQuack
Quack
/** * 呱呱叫 */ public class Quack implements QuackBehavior { @Override public void quack() { System.out.println("呱呱叫"); } }View Code
Squeak
/** * 吱吱叫 */ public class Squeak implements QuackBehavior { @Override public void quack() { System.out.println("吱吱叫"); } }View Code
MuteQuack
/** * 什麼都不做, 不會叫 */ public class MuteQuack implements QuackBehavior { @Override public void quack() { System.out.println("什麼都不做, 不會叫"); } }View Code
FlyBehavior的具體實現,用翅膀飛FlyWithWings,不會飛FlyNoWay
FlyWithWings
/** * 用翅膀飛 */ public class FlyWithWings implements FlyBehavior { @Override public void fly() { System.out.println("用翅膀飛行"); } }View Code
FlyNoWay
/** * 不會飛 */ public class FlyNoWay implements FlyBehavior { @Override public void fly() { System.out.println("什麼都不做, 不能飛"); } }View Code
然後是基類Duck,將行為接口當做實例變量放入Duck中,需要在運行時動態的改變行為,可以提供setter方法
Duck
public abstract class Duck { //針對接口編程的體現 private FlyBehavior flyBehavior; private QuackBehavior quackBehavior; //設置一個默認的行為 public Duck() { flyBehavior = new FlyWithWings(); quackBehavior = new Quack(); } public void swim(){ System.out.println("游泳"); } //每個鴨子的外觀不同, 在子類中實現 public abstract void display(); //執行呱呱叫的方法 public void performQuack(){ quackBehavior.quack(); } //執行飛行的方法 public void performFly(){ flyBehavior.fly(); } public void setFlyBehavior(FlyBehavior flyBehavior) { this.flyBehavior = flyBehavior; } public void setQuackBehavior(QuackBehavior quackBehavior) { this.quackBehavior = quackBehavior; } }View Code
注意,抽象類也是有構造方法的,不過不能創建對象,抽象類的構造方法是用來初始化變量的
兩個子類MallardDuck和RubberDuck
MallardDuck
/** * 外觀是綠色的鴨子 */ public class MallardDuck extends Duck { @Override public void display() { System.out.println("綠頭鴨"); } @Test public void test1() throws Exception { MallardDuck mallardDuck = new MallardDuck(); mallardDuck.performQuack(); mallardDuck.performFly(); display(); } }View Code
RubberDuck
/** * 橡皮鴨 * 橡皮鴨不會呱呱叫(quack), 而是吱吱叫(squeak) */ public class RubberDuck extends Duck { @Override public void display() { System.out.println("可愛的黃色橡皮鴨"); } @Test public void test1() throws Exception { Duck rubberDuck = new RubberDuck(); //運行時動態改變行為 rubberDuck.setFlyBehavior(new FlyNoWay()); rubberDuck.setQuackBehavior(new Squeak()); rubberDuck.performFly(); rubberDuck.performQuack(); rubberDuck.display(); } }View Code
封裝行為
好了,我們來看看整體的格局:飛行行為實現了FlyBehavior接口,呱呱叫行為實現了QuackBehavior接口,也請注意,我們描述事務的方式也有所改變,我們把行為看成一組算法,飛行行為是算法,呱呱叫行為也是算法,就像這樣:
我們把QuackBehavior和FlyBehavior類型的變量放到了Duck中,這其實就用到了組合。用組合創建的系統更具有彈性,不會如繼承一般一處改可能需要多處改
軟件設計原則:
多用組合,少用繼承
策略模式
好了,到這裡我們終於學到了第一個模式:策略模式(strategy pattern),介紹一下策略模式的概念
定義了算法族,並分別封裝起來,讓他們之間可以互相替換,此模式讓算法的變化獨立於使用算法的客戶