設(shè)計(jì)模式解讀 - 策略模式

    1. 模式定義
    
        
把會(huì)變化的內(nèi)容取出并封裝起來(lái),以便以后可以輕易地改動(dòng)或擴(kuò)充部分,而不影響不需要變化的其他部分;

    2. 
問(wèn)題緣起

    
當(dāng)涉及至代碼維護(hù)時(shí),為了復(fù)用目的而使用繼承,結(jié)局并不完美。對(duì)父類(lèi)的修改,會(huì)影響到子類(lèi)型。在超類(lèi)中增加的方法,會(huì)導(dǎo)致子類(lèi)型有該方法,甚至連那些不該具備該方法的子類(lèi)型也無(wú)法免除。示例,一個(gè)鴨子類(lèi)型:

    public abstract class Duck {
        //
所有的鴨子均會(huì)叫以及游泳,所以父類(lèi)中處理這部分代碼
        public void quack() {
            System.out.println("Quack");
        }
        
        public void swim() {
            System.out.println("All ducks float, even decoys.");        
        }
        
        //
因?yàn)槊糠N鴨子的外觀是不同的,所以父類(lèi)中該方法是抽象的,由子類(lèi)型自己完成。
        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 {
        //
橡皮鴨叫聲為吱吱叫,所以重寫(xiě)父類(lèi)以改寫(xiě)行為
        public void quack() {
            System.out.println("Squeak");
        }

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

    
上述代碼,初始實(shí)現(xiàn)得非常好。現(xiàn)在我們?nèi)绻oDuck.java中加入fly()方法的話(huà),那么在子類(lèi)型中均有了該方法,于是我們看到了 會(huì)飛的橡皮鴨子,你看過(guò)嗎?當(dāng)然,我們可以在子類(lèi)中通過(guò)空實(shí)現(xiàn)重寫(xiě)該方法以解決該方法對(duì)于子類(lèi)型的影響。但是父類(lèi)中再增加其它的方法呢?

    
通過(guò)繼承在父類(lèi)中提供行為,會(huì)導(dǎo)致以下缺點(diǎn):

    a. 
代碼在多個(gè)子類(lèi)中重復(fù);
    b. 
運(yùn)行時(shí)的行為不容易改變;

    c. 
改變會(huì)牽一發(fā)動(dòng)全身,造成部分子類(lèi)型不想要的改變;

    
好啦,還是剛才鴨子的例子,你也許想到使用接口,將飛的行為、叫的行為定義為接口,然后讓Duck的各種子類(lèi)型實(shí)現(xiàn)這些接口。這時(shí)侯代碼類(lèi)似于:

    public abstract class Duck {
        //
將變化的行為 fly() 以及quake()Duck類(lèi)中分離出去定義形成接口,有需求的子類(lèi)中自行去實(shí)現(xiàn)

        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();
    }

    //
野鴨子會(huì)飛以及叫,所以實(shí)現(xiàn)接口  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.");                
        }
    }

    //
紅頭鴨子會(huì)飛以及叫,所以也實(shí)現(xiàn)接口
  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.");                
        }    
    }

    //
橡皮鴨不會(huì)飛,但會(huì)吱吱叫,所以只實(shí)現(xiàn)接口
QuackBehavior
    public class RubberDuck extends Duck implements QuackBehavior{
        //
橡皮鴨叫聲為吱吱叫

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

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

    
上述代碼雖然解決了一部分問(wèn)題,讓子類(lèi)型可以有選擇地提供一些行為(例如 fly() 方法將不會(huì)出現(xiàn)在橡皮鴨中).但我們也看到,野鴨子MallardDuck.java和紅頭鴨子RedHeadDuck.java的一些相同行為代碼不能得到重復(fù)使用。很大程度上這是從一個(gè)火坑跳到另一個(gè)火坑。

    
在一段程序之后,讓我們從細(xì)節(jié)中跳出來(lái),關(guān)注一些共性問(wèn)題。不管使用什么語(yǔ)言,構(gòu)建什么應(yīng)用,在軟件開(kāi)發(fā)上,一直伴隨著的不變的真理是:需要一直在變化。不管當(dāng)初軟件設(shè)計(jì)得多好,一段時(shí)間之后,總是需要成長(zhǎng)與改變,否則軟件就會(huì)死亡。

    
我們知道,繼承在某種程度上可以實(shí)現(xiàn)代碼重用,但是父類(lèi)(例如鴨子類(lèi)Duck)的行為在子類(lèi)型中是不斷變化的,讓所有子類(lèi)型都有這些行為是不恰當(dāng)?shù)摹N覀兛梢詫⑦@些行為定義為接口,讓Duck的各種子類(lèi)型去實(shí)現(xiàn),但接口不具有實(shí)現(xiàn)代碼,所以實(shí)現(xiàn)接口無(wú)法達(dá)到代碼復(fù)用。這意味著,當(dāng)我們需要修改某個(gè)行為,必須往下追蹤并在每一個(gè)定義此行為的類(lèi)中修改它,一不小心,會(huì)造成新的錯(cuò)誤。

    
設(shè)計(jì)原則:把應(yīng)用中變化的地方獨(dú)立出來(lái),不要和那些不需要變化的代碼混在一起。這樣代碼變化引起的不經(jīng)意后果變少,系統(tǒng)變得更有彈性。

    
按照上述設(shè)計(jì)原則,我們重新審視之前的Duck代碼。

    1) 
分開(kāi)變化的內(nèi)容和不變的內(nèi)容

       Duck
類(lèi)中的行為 fly(), quack(), 每個(gè)子類(lèi)型可能有自己特有的表現(xiàn),這就是所謂的變化的內(nèi)容。
           Duck
類(lèi)中的行為 swim() 每個(gè)子類(lèi)型的表現(xiàn)均相同,這就是所謂不變的內(nèi)容。

       
我們將變化的內(nèi)容從Duck()類(lèi)中剝離出來(lái)單獨(dú)定義形成接口以及一系列的實(shí)現(xiàn)類(lèi)型。將變化的內(nèi)容定義形成接口可實(shí)現(xiàn)變化內(nèi)容和不變內(nèi)容的剝離。其實(shí)現(xiàn)類(lèi)型可實(shí)現(xiàn)變化內(nèi)容的重用。這些實(shí)現(xiàn)類(lèi)并非Duck.java的子類(lèi)型,而是專(zhuān)門(mén)的一組實(shí)現(xiàn)類(lèi),稱(chēng)之為"行為類(lèi)"。由行為類(lèi)而不是Duck.java的子類(lèi)型來(lái)實(shí)現(xiàn)接口。這樣,才能保證變化的行為獨(dú)立于不變的內(nèi)容。于是我們有:

       
變化的內(nèi)容:

       //
變化的 fly() 行為定義形成的接口
       public interface FlyBehavior {
        void fly();
       }
         
       //
變化的 fly() 行為的實(shí)現(xiàn)類(lèi)之一
       public class FlyWithWings implements FlyBehavior {
        public void fly() {
            System.out.println("I'm flying.");
        }
       }

       //
變化的 fly() 行為的實(shí)現(xiàn)類(lèi)之二
       public class FlyNoWay implements FlyBehavior {
        public void fly() {
            System.out.println("I can't fly.");
        }
       }

           -----------------------------------------------------------------

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

       //
變化的 quack() 行為實(shí)現(xiàn)類(lèi)之一
       public class Quack implements QuackBehavior {
        public void quack() {
            System.out.println("Quack");
        }
       }

       //
變化的 quack() 行為實(shí)現(xiàn)類(lèi)之二
       public class Squeak implements QuackBehavior {
        public void quack() {
            System.out.println("Squeak.");
        }
       }

       //
變化的 quack() 行為實(shí)現(xiàn)類(lèi)之三
       public class MuteQuack implements QuackBehavior {
        public void quack() {
            System.out.println("<< Slience >>");
        }
       }

       
通過(guò)以上設(shè)計(jì),fly()行為以及quack()行為已經(jīng)和Duck.java沒(méi)有什么關(guān)系,可以充分得到復(fù)用。而且我們很容易增加新的行為既不影響現(xiàn)有的行為,也不影響Duck.java。但是,大家可能有個(gè)疑問(wèn),就是在面向?qū)ο笾行袨椴皇求w現(xiàn)為方法嗎?為什么現(xiàn)在被定義形成類(lèi)(例如Squeak.java)?在OO中,類(lèi)代表的"東西"一般是既有狀態(tài)(實(shí)例變量)又有方法。只是在本例中碰巧"東西"是個(gè)行為。既使是行為,也有屬性及方法,例如飛行行為,也需要一些屬性記錄飛行的狀態(tài),如飛行高度、速度等。

    2) 
整合變化的內(nèi)容和不變的內(nèi)容

       Duck.java
 fly()以及quack()的行為委托給行為類(lèi)處理。

       
不變的內(nèi)容:

       public abstract class Duck {
            //
將行為類(lèi)聲明為接口類(lèi)型,降低對(duì)行為實(shí)現(xiàn)類(lèi)型的依賴(lài)
        FlyBehavior flyBehavior;
        QuackBehavior quackBehavior
;

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

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

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

       Duck.java
不關(guān)心如何進(jìn)行 fly()以及quack(), 這些細(xì)節(jié)交由具體的行為類(lèi)完成。
       
       public class MallardDuck extends Duck{
        public MallardDuck() {
            flyBehavior=new FlyWithWings();
            quackBehavior=new Quack();        
        }
        
        public void display() {
            System.out.println("Green head.");
        }
       }

           
測(cè)試類(lèi):

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

       
Duck.java子類(lèi)型MallardDuck.java的構(gòu)造方法中,直接實(shí)例化行為類(lèi)型,在編譯的時(shí)侯便指定具體行為類(lèi)型。當(dāng)然,我們可以:
       
       1) 
我們可以通過(guò)工廠模式或其它模式進(jìn)一步解藕(可參考后續(xù)模式講解);
       2) 
或做到在運(yùn)行時(shí)動(dòng)態(tài)地改變行為。


    3) 
動(dòng)態(tài)設(shè)定行為

       
在父類(lèi)Duck.java中增加設(shè)定行為類(lèi)型的setter方法,接受行為類(lèi)型對(duì)象的參數(shù)傳入。為了降藕,行為參數(shù)被聲明為接口類(lèi)型。這樣,既便在運(yùn)行時(shí),也可以通過(guò)調(diào)用這二個(gè)方法以改變行為。

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

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

           
測(cè)試類(lèi):


       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接口的實(shí)現(xiàn)類(lèi)型。而子類(lèi)型可通過(guò)調(diào)用setQuackBehavior(...)方法動(dòng)態(tài)改變。至此,在Duck.java增加新的行為給我們代碼所帶來(lái)的困繞已不復(fù)存在。

    
該是總結(jié)的時(shí)侯了,讓我們從代碼的水中浮出來(lái),做一只在水面上自由游動(dòng)的鴨子吧:

    3.  
解決方案


        MallardDuck 
繼承  Duck抽象類(lèi);          -> 不變的內(nèi)容
        FlyWithWings 
實(shí)現(xiàn) FlyBehavior接口;     -> 變化的內(nèi)容,行為或算法
    
Duck.java提供setter方法以裝配關(guān)系;    -> 動(dòng)態(tài)設(shè)定行為

    
以上就是策略模式的實(shí)現(xiàn)三步曲。接下來(lái),讓我們透過(guò)步驟看本質(zhì):
    
    1) 
初始,我們通過(guò)繼承實(shí)現(xiàn)行為的重用,導(dǎo)致了代碼的維護(hù)問(wèn)題。          -> 繼承
, is a
    2) 
接著,我們將行為剝離成單獨(dú)的類(lèi)型并聲明為不變內(nèi)容的實(shí)例變量并通過(guò)  -> 組合
, has a
       setter
方法以裝配關(guān)系;


        
繼承,可以實(shí)現(xiàn)靜態(tài)代碼的復(fù)用;組合,可以實(shí)現(xiàn)代碼的彈性維護(hù);使用組合代替繼承,可以使代碼更好地適應(yīng)軟件開(kāi)發(fā)完后的需求變化。

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