(轉)組合優於繼承---設計模式之策略模式

文章來源:http://www.javaeye.com/topic/328262java

當咱們掌握了Java的語法,當咱們瞭解了面向對象的封裝、繼承、多態等特性,當咱們能夠用Swing、Servlet、JSP技術構建桌面以及Web應用,不意味着咱們能夠寫出面向對象的程序,不意味着咱們能夠很好的實現代碼複用,彈性維護,不意味着咱們能夠實如今維護、擴展基礎上的代碼複用。一把刀,可使你制敵於無形而於江湖揚名,也能夠只是一把利刃而使你切菜平靜。Java,就是這把刀,它的威力取決於你使用的方式。當咱們陷入無盡無止重複代碼的泥沼,當咱們面臨牽一髮而動全身的維護惡夢, 你應該想起「設計模式」這個行動祕笈。面向對象的精義,看似平淡,其實要通過艱苦實踐才能成功。而構造OO系統的隱含經驗因而被前人蒐集而成並冠以「設計模式」之名。咱們應該在編碼行動初始就攜帶以它。接下來,讓咱們步「四人組」先行者以後,用中國文字、用實際案例領略模式於咱們代碼面目一新的改變:算法

 

設計模式解讀之一: 策略模式編程

    1. 模式定義
    
        把會變化的內容取出並封裝起來,以便之後能夠輕易地改動或擴充部分,而不影響不須要變化的其餘部分;

    2. 問題緣起

    當涉及至代碼維護時,爲了複用目的而使用繼承,結局並不完美。對父類的修改,會影響到子類型。在超類中增長的方法,會致使子類型有該方法,甚至連那些不應具有該方法的子類型也沒法免除。示例,一個鴨子類型:
設計模式

    public abstract class Duck {
        //全部的鴨子均會叫以及游泳,因此父類中處理這部分代碼
        public void quack() {
            System.out.println("Quack");
        }
        
        public void swim() {
            System.out.println("All ducks float, even decoys.");        
        }
        
        //由於每種鴨子的外觀是不一樣的,因此父類中該方法是抽象的,由子類型本身完成。
        public abstract void display();
    }

    public class MallardDuck extends Duck {
        //野鴨外觀顯示爲綠頭
        public void display() {
            System.out.println("Green head.");
        }
    }

    public class RedHeadDuck extends Duck {
        //紅頭鴨顯示爲紅頭
        public void display() {
            System.out.println("Red head.");
        }
    }

    public class RubberDuck extends Duck {
        //橡皮鴨叫聲爲吱吱叫,因此重寫父類以改寫行爲
        public void quack() {
            System.out.println("Squeak");
        }

        //橡皮鴨顯示爲黃頭
        public void display() {
            System.out.println("Yellow head.");
        }
    }

上述代碼,初始實現得很是好。如今咱們若是給Duck.java中加入fly()方法的話,那麼在子類型中均有了該方法,因而咱們看到了 會飛的橡皮鴨子,你看過嗎?固然,咱們能夠在子類中經過空實現重寫該方法以解決該方法對於子類型的影響。可是父類中再增長其它的方法呢?

    經過繼承在父類中提供行爲,會致使如下缺點:

    a. 代碼在多個子類中重複;
    b. 運行時的行爲不容易改變;
    c. 改變會牽一髮動全身,形成部分子類型不想要的改變;

    好啦,仍是剛纔鴨子的例子,你也許想到使用接口,將飛的行爲、叫的行爲定義爲接口,而後讓Duck的各類子類型實現這些接口。這時侯代碼相似於:測試

    public abstract class Duck {
        //將變化的行爲 fly() 以及quake()從Duck類中分離出去定義造成接口,有需求的子類中自行去實現

        public void swim() {
            System.out.println("All ducks float, even decoys.");        
        }
        
        public abstract void display();
    }

    //變化的 fly() 行爲定義造成的接口
    public interface FlyBehavior {
        void fly();
    }

    //變化的 quack() 行爲定義造成的接口
    public interface QuackBehavior {
        void quack();
    }

    //野鴨子會飛以及叫,因此實現接口  FlyBehavior, QuackBehavior
    public class MallardDuck extends Duck implements FlyBehavior, QuackBehavior{
        public void display() {
            System.out.println("Green head.");
        }

        public void fly() {
            System.out.println("Fly.");                
        }

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

    //紅頭鴨子會飛以及叫,因此也實現接口  FlyBehavior, QuackBehavior
    public class RedHeadDuck extends Duck implements FlyBehavior, QuackBehavior{
        public void display() {
            System.out.println("Red head.");
        }    

        public void fly() {
            System.out.println("Fly.");                
        }

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

    //橡皮鴨不會飛,但會吱吱叫,因此只實現接口QuackBehavior
    public class RubberDuck extends Duck implements QuackBehavior{
        //橡皮鴨叫聲爲吱吱叫
        public void quack() {
            System.out.println("Squeak");
        }

        //橡皮鴨顯示爲黃頭
        public void display() {
            System.out.println("Yellow head.");
        }
    }

上述代碼雖然解決了一部分問題,讓子類型能夠有選擇地提供一些行爲(例如 fly() 方法將不會出如今橡皮鴨中).但咱們也看到,野鴨子MallardDuck.java和紅頭鴨子RedHeadDuck.java的一些相同行爲代碼不能獲得重複使用。很大程度上這是從一個火坑跳到另外一個火坑。

    在一段程序以後,讓咱們從細節中跳出來,關注一些共性問題。無論使用什麼語言,構建什麼應用,在軟件開發上,一直伴隨着的不變的真理是:須要一直在變化。無論當初軟件設計得多好,一段時間以後,老是須要成長與改變,不然軟件就會死亡。

    咱們知道,繼承在某種程度上能夠實現代碼重用,可是父類(例如鴨子類Duck)的行爲在子類型中是不斷變化的,讓全部子類型都有這些行爲是不恰當的。咱們能夠將這些行爲定義爲接口,讓Duck的各類子類型去實現,但接口不具備實現代碼,因此實現接口沒法達到代碼複用。這意味着,當咱們須要修改某個行爲,必須往下追蹤並在每個定義此行爲的類中修改它,一不當心,會形成新的錯誤。

    設計原則:把應用中變化的地方獨立出來,不要和那些不須要變化的代碼混在一塊兒。這樣代碼變化引發的不經意後果變少,系統變得更有彈性。

    按照上述設計原則,咱們從新審視以前的Duck代碼。

    1) 分開變化的內容和不變的內容

       Duck類中的行爲 fly(), quack(), 每一個子類型可能有本身特有的表現,這就是所謂的變化的內容。
           Duck類中的行爲 swim() 每一個子類型的表現均相同,這就是所謂不變的內容。

       咱們將變化的內容從Duck()類中剝離出來單獨定義造成接口以及一系列的實現類型。將變化的內容定義造成接口可實現變化內容和不變內容的剝離。其實現類型可實現變化內容的重用。這些實現類並不是Duck.java的子類型,而是專門的一組實現類,稱之爲"行爲類"。由行爲類而不是Duck.java的子類型來實現接口。這樣,才能保證變化的行爲獨立於不變的內容。因而咱們有:

       變化的內容:this

 //變化的 fly() 行爲定義造成的接口
       public interface FlyBehavior {
        void fly();
       }
         
       //變化的 fly() 行爲的實現類之一
       public class FlyWithWings implements FlyBehavior {
        public void fly() {
            System.out.println("I'm flying.");
        }
       }

       //變化的 fly() 行爲的實現類之二
       public class FlyNoWay implements FlyBehavior {
        public void fly() {
            System.out.println("I can't fly.");
        }
       }

      //   -----------------------------------------------------------------

       //變化的 quack() 行爲定義造成的接口
       public interface QuackBehavior {
        void quack();
       }

       //變化的 quack() 行爲實現類之一
       public class Quack implements QuackBehavior {
        public void quack() {
            System.out.println("Quack");
        }
       }

       //變化的 quack() 行爲實現類之二
       public class Squeak implements QuackBehavior {
        public void quack() {
            System.out.println("Squeak.");
        }
       }

       //變化的 quack() 行爲實現類之三
       public class MuteQuack implements QuackBehavior {
        public void quack() {
            System.out.println("<< Slience >>");
        }
       }

經過以上設計,fly()行爲以及quack()行爲已經和Duck.java沒有什麼關係,能夠充分獲得複用。並且咱們很容易增長新的行爲, 既不影響現有的行爲,也不影響Duck.java。可是,你們可能有個疑問,就是在面向對象中行爲不是體現爲方法嗎?爲何如今被定義造成類(例如Squeak.java)?在OO中,類表明的"東西"通常是既有狀態(實例變量)又有方法。只是在本例中碰巧"東西"是個行爲。既使是行爲,也有屬性及方法,例如飛行行爲,也須要一些屬性記錄飛行的狀態,如飛行高度、速度等。

    2) 整合變化的內容和不變的內容

       Duck.java將 fly()以及quack()的行爲委拖給行爲類處理。

       不變的內容:編碼

       public abstract class Duck {
            //將行爲類聲明爲接口類型,下降對行爲實現類型的依賴
        FlyBehavior flyBehavior;
        QuackBehavior quackBehavior;

        public void performFly() {
            //不自行處理fly()行爲,而是委拖給引用flyBehavior所指向的行爲對象
            flyBehavior.fly();
        }

        public void performQuack() {
            quackBehavior.quack();
        }

        public void swim() {
            System.out.println("All ducks float, even decoys.");        
        }
        
        public abstract void display();
       }

       Duck.java不關心如何進行 fly()以及quack(), 這些細節交由具體的行爲類完成。
       
       public class MallardDuck extends Duck{
        public MallardDuck() {
            flyBehavior=new FlyWithWings();
            quackBehavior=new Quack();        
        }
        
        public void display() {
            System.out.println("Green head.");
        }
       }

 測試類:spa

public class DuckTest {
        public static void main(String[] args) {
            Duck duck=new MallardDuck();
            duck.performFly();
            duck.performQuack();        
        }
       }

 在Duck.java子類型MallardDuck.java的構造方法中,直接實例化行爲類型,在編譯的時侯便指定具體行爲類型。固然,咱們能夠:
       
       1) 咱們能夠經過工廠模式或其它模式進一步解藕(可參考後續模式講解);
       2) 或作到在運行時動態地改變行爲。

    3) 動態設定行爲

       在父類Duck.java中增長設定行爲類型的setter方法,接受行爲類型對象的參數傳入。爲了降藕,行爲參數被聲明爲接口類型。這樣,既便在運行時,也能夠經過調用這二個方法以改變行爲。
設計

public abstract class Duck {
        //在剛纔Duck.java中加入如下二個方法。
        public void setFlyBehavior(FlyBehavior flyBehavior) {
            this.flyBehavior=flyBehavior;
        }
        
        public void setQuackBehavior(QuackBehavior quackBehavior) {
            this.quackBehavior=quackBehavior;
        }

        //其它方法同,省略...
       }

測試類:code

public class DuckTest {
        public static void main(String[] args) {
            Duck duck=new MallardDuck();
            duck.performFly();
            duck.performQuack();
            duck.setFlyBehavior(new FlyNoWay());
            duck.performFly();
        }
       }

 

 若是,咱們要加上火箭助力的飛行行爲,只需再新建FlyBehavior.java接口的實現類型。而子類型可經過調用setQuackBehavior(...)方法動態改變。至此,在Duck.java增長新的行爲給咱們代碼所帶來的困繞已不復存在。

    該是總結的時侯了,讓咱們從代碼的水中浮出來,作一隻在水面上自由遊動的鴨子吧:

    3.  解決方案

        MallardDuck 繼承  Duck抽象類;          -> 不變的內容
        FlyWithWings 實現 FlyBehavior接口;     -> 變化的內容,行爲或算法
    在Duck.java提供setter方法以裝配關係;    -> 動態設定行爲

    以上就是策略模式的實現三步曲。接下來,讓咱們透過步驟看本質:
    
    1) 初始,咱們經過繼承實現行爲的重用,致使了代碼的維護問題。          -> 繼承, is a
    2) 接着,咱們將行爲剝離成單獨的類型並聲明爲不變內容的實例變量並經過  -> 組合, has a
       setter方法以裝配關係;

        繼承,能夠實現靜態代碼的複用;組合,能夠實現代碼的彈性維護;使用組合代替繼承,可使代碼更好地適應軟件開發完後的需求變化。

    策略模式的本質:少用繼承,多用組合

 

另外總結以下:

1.經過繼承實現的代碼複用容易引發牽一髮而動全身的弊端。並且在父類中添加方法可使不用擁有此方法的類也擁有此方法。 
2.經過組合實現的代碼複用更具彈性,並且便於擴展。 
3.面向抽象編程能夠提升程序的複用率。增長靈活性。 4.策略模式的核心是,將問題的可變性和不可變性分開處理,將可變性單獨抽取成接口,而且按照具體策略對其進行不一樣的實現,再經過類的組合達到代碼複用,避免繼承複用代碼,每當須要添加新的方法是,就添加對應的接口和實現類,解決繼承形成的代碼複用問題。

相關文章
相關標籤/搜索