Head First 設計模式(1)-----策略模式

吉祥發表於2019-03-22

本文參照《Head First 設計模式》,轉載請註明出處 對於整個系列,我們按照這本書的設計邏輯,使用情景分析的方式來描述,並且穿插使用一些問題,總結的方式來講述。並且所有的開發原始碼,都會託管到github上。 專案地址:github.com/jixiang5200…

1.引文

Joe的公司是做模擬鴨子活動的遊戲而出名,這款遊戲取名為SimUDuck,這款遊戲具有非常多的鴨子,一邊游泳一邊呱呱叫。這裡的設計採用了標準的OO(Object Oriented,物件導向)的方式編寫,這裡有一個鴨子的超類(SuperClass),後續所有的鴨子都必須繼承這個超類。

  • OO物件導向模型

鴨子OO模型

  • 繼承

這時,公司的高層們想要通過模擬會飛的鴨子來追求行業的領先。然後Joe的專案經理拍著胸脯告訴主管,Joe很快就可以搞定,“有了OO什麼都不怕” Joe接收到任務後,想出了一個辦法:“我僅需要在Duck這個超類中加上Fly()的方法,然後所有的鴨子都可以飛了”。然後他的設計模型就改成以下的樣子

增加fly方法後的模型
這樣看起來貌似沒有什麼問題的,然後可怕的問題發生了。。。。 使用者反饋,自己的橡皮鴨和木鴨居然也可以飛起來!!! 那麼,到底是為何導致了這個可怕的問題? 我們來分析一下:由於Joe在Duck超類中加上fly方法,導致所有的子類都會繼承該方法,這就導致了原本不會飛的橡皮鴨和木鴨也具有飛行能力,顯然為了提高複用性使用的繼承方式,並未達到完美得結果。

  • 繼承+覆蓋

Joe在思考後,又得出一個方案,那就是在橡皮鴨中將fly方法覆蓋掉,不做任何操作,這樣原有的RubberDuck類中架構就變成如下:

RubberDuck
然後,業務的需求又需要加入木鴨(DecoyDuck),它不會叫也不會飛。苦逼的Joe又要把木鴨(DecoyDuck)中quark方法覆蓋,這樣DecoyDuck中的類結構就變成如下:
DecoyDuck

但是我們會發覺,如果以後有新的業務,甚至於fly方法中出現一個bug,或者需要刪除fly相關的業務,所有相關程式碼都需要修改,在大型專案中,這都導致非常可怕的維護問題。那麼,到底該怎麼辦

  • 介面

“這不就相當於讓我根據使用者手機殼顏色換主題嗎?”,可憐的Joe在快被想要衝上去打產品經理的時候(皮一下。。。)。腦子突然想起一件神器,他決定試試。他的方法就是使用介面(Interface),將fly分離出來,放進一個Flyable介面中。針對於quark方法,也可以這樣分離進Quarkable介面中,

介面

雖然這樣看起來滿足了我們之前所提到的所有需求,但是這樣一來重複程式碼量會非常可怕,如果有上萬個Duck子類,Joe一定會發瘋的。所有,這個方法雖然看起來很好,但是一旦某個方法或者行為發生改變,我們需要定位到所有實現該方法的類中去修改對應的程式碼,這很容易導致bug的發生。

那麼,到底有沒有一種能夠建立軟體的有效方法,能夠讓我們可以對既有的專案在影響最小的情況下修改他的業務邏輯,這樣我們能夠花很少的時間去修改程式碼。

#2.策略模式

第一設計原則 找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的程式碼混合在一起。

按照以上的原則進行設計,程式碼發生變化引起的後果會非常小,整個專案會特別具有彈性。 這個原則不僅僅適用於策略模式,對於之後講解的模式同樣也是核心的指導方向。那麼我們繼續Joe所遇到的Duck問題。

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

我們很清楚,Duck類內部的fly()和quark()伴隨著鴨子的不同會發生改變,其他模組是不變的。為了要把這兩個變化的行為從Duck類中分開,我們把它們從Duck類中抽離出來,建議一組新類用來代表每一個行為。

Duck可變不可變分析

那麼,問題來了。如何設計那組實現飛行行為和呱呱叫行為的一組類呢? 這裡就需要提及第二設計原則

第二設計原則 針對於介面程式設計,不針對實現程式設計

這裡我們希望的是在建立具體的Duck類的時候,可以動態的生成對應的行為。打個比方,我們想要產生一個新的綠頭鴨,將制定型別的飛行行為賦予給它。這就說明,在Duck類中,我們需要包含定義Duck行為的方法,這樣在執行的時候,我們就可以動態去改變綠頭鴨的行為。 所以這裡我們使用兩個介面來代表兩個行為,這裡定義為 FlyBehavior和QuarkBehavior,行為的每次實現,都將實現對應的介面。

但是介面類是沒有方法體的,也就是說,我們需要一組類實現對應的行為,這些專用來實現類似FlyBehavior和QuarkBehavior的一組類,我們稱為行為類

這裡提到的介面類並非嚴格意義上Java中的介面(Interface),可以理解為抽象類或介面。這裡我們可以理解為:

"針對介面程式設計"真正的意思是“針對超型別(supertype)程式設計”

這裡講得有點難以理解,我們對比一下針對實現程式設計和針對介面程式設計的區別:

//針對實現程式設計
Dog dog=new Dog();
dog.bark();

//針對介面程式設計
Animal animal=new Dog();
animal.makeSoud();
複製程式碼

2.2實現鴨子的行為(程式碼)

從上面的講解,我們可以理解可變部分是fly和quack兩種方法。不可變為Duck類。那麼這裡我們需要使用兩個介面FlyBehavior和QuackBehavior,還有一些列他們對應的行為類,具體的結構邏輯如下圖:

鴨子行為

這設計有兩個很明顯的優勢:

  • 可以讓飛行和呱呱的叫的動作可以被其他物件複用,因為這些行為已經與鴨子類無關。也就是解耦

  • 我們能夠在不影響原有的行為類的情況下新增一些行為。也就是具備了彈性可擴充性

擴充幾個概念:

耦合指的就是兩個類之間的聯絡的緊密程度 解耦指的是解除類之間的直接關係,將直接關係轉換成間接關係 想要了解的可以參考這篇文章:blog.csdn.net/qq_24499615… 接下來分別將FlyBehavior,FlyWithWings,FlyNoWay分別貼下

public interface FlyBehavior {

    //飛行
    public void fly();
}
複製程式碼
public class FlyWithWings implements FlyBehavior{

    public void fly() {
      System.out.println("I am flying!");  
    }

}
複製程式碼
public class FlyNoWay implements FlyBehavior {

    public void fly() {
       System.out.println("I can't fly!"); 

    }

}
複製程式碼

接下來將QuackBehavior,Quack,MuteQuack,Squeak類的程式碼分別貼下:

public interface QuackBehavior {
    //呱呱叫
    public void quack();
}
複製程式碼
public class Quack implements QuackBehavior {

    public void quack() {
       System.out.println("Quack");

    }

}
複製程式碼
public class MuteQuack implements QuackBehavior {

    public void quack() {
        System.out.println("<< Slience>>");

    }

}
複製程式碼
public class Squeak implements QuackBehavior {

    public void quack() {
       System.out.println("Squeak");
    }

}
複製程式碼

到這裡將fly和quack的介面類和行為類完成。

2.3組合鴨子行為

在前面2.2我們將飛行(fly)和呱呱叫(quack)的動作"委託"(delegate)給其他介面類處理,而並非在Duck類(或者子類)中定於fly和quack方法。那麼到底該怎麼把行為組合進Duck中?

  • 1.首先在Duck類中增加兩個“例項變數”,分別為flyBehavior和quackBehavior,宣告為介面型別(注意不是具體類的實現型別),每個Duck(或其子類)會動態的設定這些變數以在執行時引用正確的行為型別(如FlyWithWings,Squeak等)。Duck類的類結構如下

    Duck類結構

  • 2.那麼,就開始實現Duck類

public abstract class Duck {
    //為行為介面型別宣告兩個引用變數,所有的鴨子(或子類)都繼承它們
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    
    public Duck(){
        
    }
    
    public abstract void display();
    
    public void performQuck(){
        //委託給行為處理
        quackBehavior.quack();
    }
    
    public void performFly(){
      //委託給行為處理
        flyBehavior.fly();
    }
    
    public void swim(){
        System.out.println("All ducks float");
    }
}
複製程式碼

然後我們實現一個MallardDuck類來實現組合,

public class MallardDuck extends Duck{
    
    public MallardDuck(){
        //使用FlyWithWings作為其FlyBehavior型別
        flyBehavior=new FlyWithWings();
        //綠頭鴨使用Quck類處理呱呱叫,
        //所以當performQuack被呼叫時,叫這個行為被委託給Quck物件
        quackBehavior=new Quack();
    }
    
    /*
     * 因為MallardDuck繼承自Duck類
     * ,所以具備flyBehavior與quackBehavior例項變數
     */

    public void display() {
        // TODO Auto-generated method stub
        
    }
}
複製程式碼

當然構造器內還是需要實現具體行為類,這在之後的模式中會提供相應的解決方案,之後我們會迴歸到這個問題繼續解決這個問題。

到這裡,組合鴨子類已經實現。

  • 3.測試效果 這裡我們編譯測試類
public class MiniDuckSimilator {
    
    public static void main(String[] args) {
        Duck mallerdDuck=new MallardDuck();
        //一下程式碼是將具體的行為委託給對應的行為類處理行為
        mallerdDuck.performQuck();
        mallerdDuck.performFly();
                
    }

}
複製程式碼

執行結果

#2.4 動態行為設定 在之前的實現中我們是在Duck的具體子類中實現FlyBehavior和QuackBehavior的行為,但是Duck失去了動態設定的功能,對於追求完美的程式設計師來說是不可饒恕的。所以急切需要通過一個方法動態設定行為,而並非是在鴨子(Duck)的構造器中去例項化。這裡推薦一個方法-----設定方法(setter method)

    1. 在Duck類中增加兩個新方法 setFlyBehavior()和setQuckBehavior().對於Duck的類結構修改如下
      image.png
      具體修改如下
public abstract class Duck {
    //為行為介面型別宣告兩個引用變數,所有的鴨子(或子類)都繼承它們
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    
    public Duck(){
        
    }
    
    public abstract void display();
    
    public void performQuck(){
        //委託給行為處理
        quackBehavior.quack();
    }
    
    public void performFly(){
      //委託給行為處理
        flyBehavior.fly();
    }
    
    public void setFlyBehavior(FlyBehavior flyBehavior){
        this.flyBehavior=flyBehavior;
    }
    
    public void setQuackBehavior(QuackBehavior quackBehavior){
        this.quackBehavior=quackBehavior;
    }
    
    public void swim(){
        System.out.println("All ducks float");
    }
}
複製程式碼
  • 2.建立一個新的鴨子模型:模型鴨(ModelDuck)
public class ModelDuck extends Duck{
    
    public ModelDuck(){
        flyBehavior=new FlyNoWay();
        quackBehavior=new Quack();
    }

    public void display() {
       System.out.println("I'm a model duck");
        
    }

}
複製程式碼
  • 3.新建立一個新的FlyBehavior型別 FlyRocketPowered
public class FlyRocketPowered implements FlyBehavior{

    public void fly() {
      System.out.println("I'm flying with a rocket!");
        
    }

}
複製程式碼
  • 4.修改測試類MiniDuckSimulator,加上模型鴨,並令模型鴨具備火箭動力
public class MiniDuckSimilator {
    
    public static void main(String[] args) {
        Duck mallerdDuck=new MallardDuck();
        //一下程式碼是將具體的行為委託給對應的行為類處理行為
        mallerdDuck.performQuck();
        mallerdDuck.performFly();
        
        Duck modelDuck=new ModelDuck();
        //第一次會使用構造引數裡的飛航模式
        modelDuck.performFly();
        modelDuck.setFlyBehavior(new FlyRocketPowered());
        //模型鴨具備火箭飛行能力
        modelDuck.performFly();
                
    }

}
複製程式碼

執行結果:

執行結果

到這裡我們發現鴨子模型中我們使用到類的組合使用,而這裡我們涉及到第三個設計原則:

第三個設計原則 多用組合,少用繼承

正如我們所見,組合所建立的系統具備極大的彈性,不僅僅可以將行為封裝為一系列的行為類,更可以動態改變行為,只需要組合的行為物件是符合正確的行為介面標準的。

3.策略模式講解

總結之前的三個設計原則:

第一設計原則 找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的程式碼混合在一起。

第二設計原則 針對於介面程式設計,不針對實現程式設計

第三設計原則 多用組合,少用繼承

總結這三條原則結合起來就是我們學習的第一個模式:

策略模式 定義了演算法族,分別封裝起來,讓它們之間可以相互替換,此模式讓演算法的變化獨立於使用演算法的客戶。

相關文章