設計模式:簡單的鴨子模型(入門)

aFa發表於2019-02-23

這是我第一次寫文章,可能有寫得不好的地方請大佬指正

可能這會是一篇長期連載的設計模式系列哈哈哈

搬至 head First 設計模式 ,這本書真的非常有意思和易懂

首先 要從設計模式入門 ,需要先看一個簡單的模擬鴨子應用開始

JOE的公司做了一套相當成功的模擬鴨子的遊戲,遊戲中會出現各種鴨子 ,一邊游泳戲水,一邊呱呱叫,此係統的內部設計使用了標準的OO設計,設計了一個鴨子的超類(superclass),並讓各種鴨子繼承此超類

設計模式:簡單的鴨子模型(入門)

所有的鴨子都會呱呱叫和游泳,所以讓超類負責處理這部分的實現程式碼

每個鴨子的子型別負責實現自己的display()行為在螢幕上顯示其外觀

設計模式:簡單的鴨子模型(入門)設計模式:簡單的鴨子模型(入門)

現在主管們決定,需要模擬程式需要會飛的鴨子,在這個時候,Joe告訴主管們,他一個星期就能搞定,這有什麼困難?Joe 需要在duck類中假如fly()方法,然後所有鴨子就會飛了,他很高興的去交差設計模式:簡單的鴨子模型(入門)

但是可怕的事情發生了,老闆在看的時候發現很多的橡皮鴨子在螢幕上飛來飛去,於是通知他準備另尋工作了。

原來,Joe忽略了一件事,並非Duck所有的子類都會飛,Joe在Duck類上加上新的行為,會使某些並不適合該行為的子類也具有該行為。現在可好,程式中有一個無生命會飛的東西。

對程式碼所做的區域性修改,影響層面可不只是局面(比如會飛的橡皮鴨)

他體會到了一件事,當涉及“維護”時,為了複用目的而使用繼承的結局並不完美。

public class RubberDuck extends Duck {
    @Override
    void display() {
        System.out.println("我是橡皮鴨");
    }
    @Override
    public void quack() {
        System.out.println("修改為吱吱叫");
    }
}複製程式碼

當前結構:

設計模式:簡單的鴨子模型(入門)


Joe突然想到,只要像quack()方法一樣,把fly()方法覆蓋掉就可以了。

@Override
void fly() {
    // 什麼事都不做
}複製程式碼

可是又衍生出另一個問題,那我以後要加個木頭鴨呢,不會飛也不會叫。

public class DecoyDuck extends Duck {

    @Override
    void display() {
        System.out.println("我是木頭鴨");
    }

    @Override
    public void quack() {
        // 什麼事都不做
    }

    @Override
    void fly() {
        // 什麼事都不做
    }
}複製程式碼

每次有新的鴨子的子類出現,他就要被迫檢查並可能需要覆蓋掉fly()和quack() ,這簡直是無窮無盡的噩夢。。。

所以,他需要一個更清晰的方法,讓某些鴨子型別可飛或可叫

有一個想法: 把fly()從超類中取出來,這麼一來,只有會飛的鴨子實現flyable介面,同樣的方法可以用在quack(),設計一個quackable介面

public interface Quackable {
    void quack();
}複製程式碼
public interface Flyable {
    void fly();
}
複製程式碼

實現為繼承  虛線為實現

設計模式:簡單的鴨子模型(入門)

大家覺得這個設計如何?


可想而知 ,這是一個超笨的方法,這樣一來重複的程式碼會變多,萬一需要修改48個duck的子類的fly行為,你又需要全部都修改了。

我們知道繼承並不是適當的 解決方式,雖然flyable 和quackable可以解決一部分問題,但是造成了程式碼無法複用,這隻能說是從一個噩夢跳到另一個噩夢了。

接著往下看

軟體開發的一個不變真理:不管軟體當初設計得多好,一段時間總是需要成長與改變,否則軟體就會死亡。


我們把問題歸零,現在我們知道繼承並不能很好的解決我們的問題,flyable介面也是。幸運的是有一個設計原則(不是設計模式) 可以幫我們很好的解決這個問題。

設計原則1:找到應用中可能需要變化的地方,把它們獨立出來,不要和那些不需要變化的程式碼混在一起。(這是我們的第一個設計原則,後面還會有)

換句話說,每次新的需求一來,都會使某方面的程式碼發生改變,那麼你就可以確定,這段程式碼需要被抽出來。把需要變化的部分抽取出來並封裝起來,以便以後可以輕易改變或者擴充這部分,不影響不需要變化的部分。

回到我們的問題,把鴨子的行為fly 和quack從duck類中取出。

設計鴨子的行為,我們希望一切能有彈性,我們應該在鴨子類中包含設定行為的方法,這樣就可以在執行時動態的改變綠頭鴨的飛行行為。

有了這些目標,接下來看看第二個設計原則:

設計原則2:針對介面變成,而不是針對實現程式設計

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

我們利用介面代表每個行為,比方說,flyBehavior 和quackBehavior,而行為的每個實現都將實現其中的一個介面。所以這次鴨子類不會實現flyable和quackable介面,反而由我們製造一組其他類專門實現flyBehavior 和quackBehavior,這個就被稱為行為類。由行為類而不是duck類來實現行為介面。

public interface FlyBehavior {
    void fly();
}複製程式碼

public class FlyCanWay implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("會飛");
    }
}複製程式碼

public class FlyNotWay implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("不會飛");
    }
}複製程式碼

public interface QuackBehavior {
    void quack();
}複製程式碼
public class Quack implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("呱呱叫");
    }
}複製程式碼
public class Squeak implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("吱吱叫");
    }
}複製程式碼

這樣的設計,可以讓飛行和呱呱叫的動作被其他的物件複用,因為這些行為已經與鴨子類無關了,而我們可以新增一些行為,不會影響到既有的行為,也不會影響使用到飛行的鴨子類


現在開始整合鴨子的行為

做法是這樣:

首先在duck類中加入兩個例項變數,為別為flyBehavior和quackBehavior,每個物件都會動態的設定這些變數以執行時引用正確的行為型別。

我們找兩個類似的方法performFly()和performQuack()取代duck中的fly()和quack()。往下看

@Data
public abstract class Duck {
    /* 鴨子游泳 */
    public void swim(){}
    /* 鴨子形狀  可能有很多種類的鴨子,所以display方法是抽象的 */
    abstract void display();

    FlyBehavior flyBehavior;

    QuackBehavior quackBehavior;

    void performFly(){
        // 每隻鴨子都會引用實現FlyBehavior介面的物件
        flyBehavior.fly();
    }

    void performQuack(){
        quackBehavior.quack();
    }
}複製程式碼


現在我們來關心如何設定flyBehavior和quackBehavior變數

public class MallardDuck extends Duck {
    @Override
    void display() {
        System.out.println("我是綠頭鴨");
    }
    /**
     *  因為繼承了duck 所以擁有這兩個變數
     */
    public MallardDuck() {
        quackBehavior = new Quack();
        flyBehavior = new FlyCanWay();
    }
}複製程式碼

綠頭鴨使用quack類處理呱呱叫,所以當performQuack被呼叫時,叫的職責被委託給quack物件,我們就得到真正的呱呱叫。

看懂了嗎? 當
MallardDuck例項化時,它的構造器會把繼承過來的 quackBehavior例項變數初始化為quack型別的新例項,flyBehavior同理。

 測試一下

public static void main(String[] args) {
    Duck mallardDuck = new MallardDuck();
    mallardDuck.performFly();
    mallardDuck.performQuack();
}複製程式碼

結果

設計模式:簡單的鴨子模型(入門)

如何讓鴨子具有動態行為呢?

利用flyBehavior的setter方法,我們可以隨時呼叫setter改變鴨子的行為。

public void setFlyBehavior(FlyBehavior flyBehavior) {
    this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
    this.quackBehavior = quackBehavior;
}複製程式碼

加入我們的新鴨子種類

public class ModelDuck extends Duck {

    @Override
    void display() {
        System.out.println("我是一隻橡皮鴨");
    }

    public ModelDuck() {
        flyBehavior = new FlyNotWay();
        quackBehavior = new Squeak();
    }
}複製程式碼

此時,我們讓飛擁有火箭飛,和普通飛兩種,實現flyByhavior介面

public class FlyRocket implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("火箭飛");
    }
}複製程式碼

只使得本來不會飛的模型鴨 變成 火箭飛

public static void main(String[] args) {
    Duck mallardDuck = new MallardDuck();
    mallardDuck.performFly();
    mallardDuck.performQuack();

    Duck modelDuck = new ModelDuck();
    modelDuck.performFly();
    modelDuck.setFlyBehavior(new FlyRocket());
    modelDuck.performFly();
}複製程式碼

結果:

會飛

 呱呱叫

 不會飛

 火箭飛

好,我們已經深入研究了鴨子模擬器的設計了。下面是模型圖

設計模式:簡單的鴨子模型(入門)

當你將繼承和實現一起組合使用時就衍生另一個設計原則:

設計原則3:多用組合(繼承加實現),少用繼承

這文到這裡就結束了,其實上面的就是第一個設計模式:也就是策略模式!

策略模式:分別封裝起來,讓他們之間可以互相替換。


總結

其實我這本書還沒看完,看了第一次懵懵懂懂,第二次看就會有很大的理解,第三次看慢慢的輪廓就出現在我的腦海中。我寫這篇文章的時候應該是我第四次邊看邊寫下來。

接下來會研究繼續寫,即使沒人看哈哈哈


相關文章