[head first 設計模式] 第一章 策略模式

alexhe101發表於2020-11-22

[head first 設計模式] 第一章 策略模式

讓我們先從一個簡單的鴨子模擬器開始講起。

假設有個簡單的鴨子模擬器,遊戲中會出現各種鴨子,此係統的原始設計如下,設計了一個鴨子超類,並讓各種鴨子繼承此超類。

1.jpg

若此時我們有了一個新的需求,我們需要鴨子會飛,那麼我們該如何修改程式碼呢?

最初,我們想在基類上加上fly方法,使得所有子類鴨子都擁有相應的fly方法。但這樣錯誤產生了,即使是本不該會飛的橡皮鴨子也擁有了fly方法。

或許我們可以把橡皮鴨中的fly方法覆蓋掉,但這樣每次新加入的不會飛的新鴨子型別,難道都要額外覆蓋一次fly方法嗎?太麻煩了。

利用繼承來提供duck的行為,會導致執行時的行為不容易改變,且改變容易牽一髮動全身。

那麼,利用介面如何?

若經常需要更新產品,那麼每次覆蓋fly簡直是噩夢。那麼,我們將fly單獨寫成一個介面,只有會飛的鴨子實現這個介面如何?

2.jpg

但這樣其實重複的程式碼會變得非常多,造成fly程式碼無法複用,每個會飛的鴨子都要實現fly方法。

那麼我們該如何解決這個問題?在使用設計模式之前,不妨先求索於OO原則!

軟體開發中,什麼是永恆真理?

唯一不變的是變化本身——約翰遜·斯賓塞

現在我們已經知道了繼承無法很好的解決問題,因為鴨子的行為在子類中不斷改變,並且有的行為子類不應該擁有。使用介面初看挺不錯的,但繼承介面無法達到程式碼的複用。這意味著,無論合適你需要修改某個行為,你必須向下追蹤並在每一個定義此行為的類中修改它。

但還好,有一個OO設計原則正好適用於此種情況:

找出系統中可能需要變化之處,把他們獨立出來,不要和那些不變化的程式碼堆在一起。

也就是把會變化的部分取出來,好讓其他部分不會受此影響。

把會變化的部分取出來並封裝,以後可以輕易地改動或擴充此部分,而不影響其他不需要變化的部分。

那麼,現在是時候把鴨子的行為從Duck類中取出了。

分開變化和不會變化的部分

目前而言,除了fly()和quack()以外,duck類其他部分看起來不怎麼變動,所以我們僅做些小改變。

為此,我們準備建立兩組類,一個是和fly相關的,另一個和quack相關的,每一組類都實現各自的動作。

3.jpg

設計鴨子的行為

如何設計

那組實現飛行和叫聲的類呢?我們希望一切能有彈性,並且能夠將行為指定到鴨子的例項。並且可以讓鴨子的行為動態的改變。

有了這些目標要實現,我們看第二個設計原則

針對介面程式設計,而不是針對實現程式設計。

從現在開始,鴨子的行為將被放在分開的類中,此類專門提供某行為介面的實現。這樣,鴨子類就不再需要知道行為的具體細節。

這次鴨子類不會負責實現flying和quacking介面,而是由我們製造一組其他類專門實現flybehavior和quackbeavior,這就稱為行為類,由行為類而不是duck類來實現行為介面。

這種做法和以往不同,以往是行為來自於duck超類的具體實現,或是繼承某個介面並由子類自行實現而來。這兩種方法都是依賴於實現,沒法變更行為。

在我們的新設計中,鴨子的子類將使用介面所表示的行為,所以具體的實現不會被繫結在鴨子的子類中。

4.jpg

整合鴨子的行為

關鍵在於,鴨子會將飛行和叫聲行為委託給其他物件處理。而不是由自己定義。

在Duck類中加入flyBehavior和quackBehavior變數,宣告為介面型別。每個鴨子物件動態設定這些變數以在執行時引用正確的行為型別。

程式碼如下

public interface FlyBehavior {
    public void fly();
}
public interface QuackBehavior {
    public void quack();
}
public class FlyWithWings implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("I'm flying");
    }
}
public class FlyNoWay implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("I can't fly");
    }
}
public class Quack implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("quack!");
    }
}
public class MuteQuack implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("<<silence>>");
    }
}
public class Squeak implements QuackBehavior{

    @Override
    public void quack() {
        System.out.println("Squeak!");
    }
}
public abstract class Duck {
    protected FlyBehavior flyBehavior;
    protected  QuackBehavior quackBehavior;
    abstract void display();
    public void performFly()
    {
        flyBehavior.fly();
    }
    public void performQuack()
    {
        quackBehavior.quack();
    }
}
public class MallardDuck extends Duck{
    @Override
    public void display() {
        System.out.println("I'm a real mallard duck");
    }
    public MallardDuck(){
        flyBehavior = new FlyWithWings();
        quackBehavior  = new Quack();
    }
}
public class MiniDuckSimulator {
    public static void main(String[] args) {
        Duck mallardDuck = new MallardDuck();
        mallardDuck.performFly();
        mallardDuck.performQuack();
    }
}

動態設定行為

在鴨子子類中為兩個behavior加入set方法,而不是在構造器中進行例項化。有了這個,我們就能在執行時隨時改變鴨子的行為。


    public void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    public void setQuackBehavior(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }

整體設計

現在我們來看看整體結構

5.jpg

我們不再把鴨子的行為說成是行為,而是一族演算法。演算法代表鴨子能做的事情。在本例中,我們鴨子的行為是組合來的,而不是繼承來的。

我們得到第三個OO設計原則

多用組合,少用繼承

學習完以上部分,我們正式定義策略模式

6.jpg

相關文章