策略模式---動態更改算法


      策略模式是設計模式中很重要的一種,它的主要意圖就是:定義了算法族,分別封裝起來,讓它們之間可以互相替換。它讓算法的變化可以獨立於使用算法的客戶,使得我們可以隨時更改它們而不影響客戶端的代碼,而客戶端可以自由選擇不同的算法執行。

       要想了解策略模式,我們就要理解幾個重要概念:

      1.什么是策略?
      2.什么是算法?
      3.算法可替換的條件?
 
       所謂的策略就是指在給定的輸入條件下,實現某個目標的計划或方案,而算法是一個定義好的過程,能夠根據一組輸入產生一個輸出。對於這兩者可以這樣理解:策略是一組可替換的算法。
       能夠相互替換的算法必須具有相同的特點,就是它們處理的對象的來源一樣,去向也一樣,至於對象的類型,不要求相同。
       明白了策略和算法的區別,我們接下來就要討論策略模式的幾個重要方面:
       
      1.使用策略模式的原因?
      2.策略模式的實現?
      3.策略模式的優缺點?
 
      為什么我們要使用策略模式呢?想想這樣的情況,我們負責一個基地戰斗機數據的記錄,戰斗機有各種型號和性能,性能中的武器和飛行方式又是各種各樣,但是我們提供的接口只有兩個方法:fly()和attack()。想要在這兩個方法中展現不同戰斗機的性能,我們可能會用繼承。
        首先,我們會定義一個抽象類:Plane,這個抽象類的大概樣子像是下面這樣:
public abstract class Plane{
   private abstract void fly();
   private abstract void attack();
 }

       接着我們就開始定義一些具體型號的戰斗機:

      public class RocketPlane extends Plane{
             private void fly(){
                 System.out.println("This plane fly with Rocket");
            }
            private void attack(){
                 System.out.println("This plane attack with fire");
            }
       }
       
       public class WindPlane extends Plane{
           private void fly(){
                  System.out.println("This plane fly with wind");
           }
           private void attack(){
                  System.out.println("This plane attack with Rocket");
           }
      }

          然后就在我們的程序中這樣寫:

 public static void main(String[] args){
            RocketPlane plane1 = new RocketPlane();
            WindPlane plane2 = new WindPlane();
           
            showFunctionOfPlane(plane1);
            showFunctionOfPlane(plane2);
       }
      
      private void showFunctionOfPlane(Plane plane){
             plane.fly();
             plane.attack();
      }

        使用繼承可以解決這個問題,但是,繼承也有它自己的問題。繼承最大的問題就是,基類的改變會傳給所有的子類,這是我們類設計者不想看到的。那么,不使用繼承不就可以了?接口,就是我們這種情況下最好的替代方案。

       使用接口,我們的代碼就可以更加靈活。
       接口是一個好東西,但如何使用,也是一個重要的問題,就是我們該讓什么成為接口?如果我們這里將戰斗機這個抽象作為接口,像是這樣:
      
    public Interface Plane{
         void fly();
         void attack();
     }

       這樣就將具體的實現交給實現類,從而避免我們上面的問題。確實如此,但不同型號的戰斗機,就算外觀差距太大,基本的東西都是不變的,像是重量這些基本的屬性,至少在很長的一段時間都不會發生變化,如果用接口的話,我們就不能設置一些共同的屬性和方法,當然我們可以將這樣的東西交給實現類來實現,這樣,代碼重復的程度太可怕了!

       抽象類還是有抽象類的好處,接口依然不能完全代替,就像上面的例子。接口在很大程度上都被人濫用了,因為它是一個非常好用的東西,尤其是多態的使用。但是類的設計應該貼近現實生活,就像上面戰斗機的例子,我們是對戰斗機這樣的具體東西進行抽象,而且代碼應該能體現程序員對待一個問題的思維,並不僅僅是交給計算機自己處理的字節碼。濫用接口本身就是對這種原則的破壞,因為很多人都不清楚接口的真正意義。
       使用抽象類是為了表達“is-a”關系,而使用接口是為了表達"has-a"關系。繼承自一個抽象類,子類本身就是抽象類的一個特例,在分類上它屬於抽象類,但是實現一個接口,並不能說,我們的實現類就是一個接口,准確的說法就是我們的實現類具有該接口定義的行為,當然,對於類型識別來說,接口和抽象類是沒有什么區別的,都是一個事物的抽象。接口的真正意義是一組行為協議,規定我們的實現類應該具有的行為。也許有些人會說,這樣是“is-like-a"關系,這樣的說法其實搞錯了"is-like-a"關系,"is-like-a"關系本身也屬於繼承的一種關系,傳統意義上的繼承應該是完全繼承,就是不添加新的功能,只是覆寫我們基類的方法,這樣子類就可以向上轉型為基類而不會出錯,但是,現實就是子類會有它們自己的行為,會有自己特有的屬性和行為,使得它們無法向上轉型,這就是"is-like-a"關系。
        理解好接口和抽象類的邏輯意義,我們在設計的時候就能根據現實生活來決定到底應該采用什么樣的抽象。上面的例子,我們依然使用抽象類,因為我們需要一個地方來存放所有戰斗機都具有的屬性和行為,抽象類是一個非常好的選擇。接着,我們將會改動的行為抽取出來作為接口。如何判斷一個行為是否應該抽取出來,我們就看:如果我們對該行為進行修改,相應的其他代碼是否也要進行修改,如果需要,說明這個行為是一個變化的行為因素。這里我們就抽取出飛行和攻擊這兩個行為。
        我們現在抽取出飛行和攻擊這兩個接口:
 public Interface FlyAble{
     void fly();
  }
         
  public Interface AttackAble{
      void attack();
  }
         這里我們就可能犯一個錯誤,像是這樣:
         public class RocketPlane extends Plane implements FlyAble, AttackAble{
               void fly(){
System.out.println("This plane fly with Rocket");
}
void attack(){
System.out.println("This plane attack with fire");
} }
public class WindPlane extends Plane implements FlyAble, AttackAble{ void fly(){
System.out.println("This plane fly with wind"); 
}
void attack(){
System.out.println("This plane attack with Rocket"); 
} }

         為什么會這樣寫?很簡單,因為我們可能有些飛機根本不具有飛行能力,像是這樣:

  public class NotFlyPlane extends Plane implements AttackAble{
       void attack(){
System.out.println("This Plane attack with wind");
} }

        但是,根本不需要我們的子類實現這些接口,接口更大的意義是對象組合,這樣根本就失去了接口的優點。要想利用接口的這些優點,我們可以這樣建立這兩個接口的實現類組,像是這樣:

           public class FlyWithRocket implements FlyAble{
               void fly(){
System.out.println("This plane fly with rocket");
} }
public class AttackWithRocket implements AttackAble{ void attack(){
System.out.println("This Plane attack with rocket");
} }

public class FlyWithWind implements FlyAble{
void fly(){
System.out.println("This plane fly with wind");
}
}

public class AttackWithFire implements AttackAble{
void attack(){
System.out.println("This plane attack with fire");
}
}

public class NotFly implements FlyAble{
void fly(){
System.out.println("This plane can't fly");
}
}

       然后再在我們的代碼中使用這些實現類:

 public abstract class Plane(){
protected FlyAble mFly;
protected AttackAble mAttack;

protected abstract void description();

protected void functionTest(){
mFly.fly();
mAttack.attack();
}
}

public class RocketPlane extends Plane{ RocketPlane(){
super.
mFly = new FlyWithRocket(); super.mAttack = new AttackWithFire();
}
protected void description(){
System.out.println("I am a RocketPlane");
}
}

public class test{
public static void main(String[] args){
RocketPlane plane = new RocketPlane();
plane.functionTest();
}
}

       這就是使用對象組合的方式,但是這樣的方式還不夠優雅。這時,策略模式就正式登場了,因為它就是處理對象組合的一種模式。

       我們的抽象類可以這樣修改:

public abstract class Plane {  

    private FlyAble mFly; 

    private AttackAble mAttack;

    Plane() {  }

    Plane(FlyAble fly, AttackAble attack) {   

      this.mFly = fly;  

      this.mAttack = attack;

    }

    protected abstract void description();

    protected void setFly(FlyAble fly) {   

       this.mFly = fly;

    }

    protected void setAttack(AttackAble attack) {   

       this.mAttack = attack;  

    }

    protected void testFunction() {

      description();   

      mFly.fly();  

      mAttack.attack();  

   }

}

           接着是我們的子類和測試類:

public class RocketPlane extends Plane {

    RocketPlane() { }

    RocketPlane(FlyAble fly, AttackAble attack) {   

      super(fly, attack);

    }

   @Override  

   protected void description() {   

      System.out.println("I am a RocketPlane");

   }

}

   public class test{

      public static void main(String[] args){

          RocketPlane plane = new RocketPlane(new FlyWithRocket(), new AttackWithFire());

          plane.testFunction();

      }

   }

         如果戰斗機以后的飛行能力發生變化,我們可以動態的更改它的行為,像是這樣:
plane.setFly(new FlyWithWind());
       這樣,它就從火箭噴射變成靠機翼飛行!!而且我們客戶可以隨時更換飛行能力,只要他喜歡,甚至可以是沒有飛行能力。
       以上就是策略模式的標准用法,它完全體現了策略模式的意圖。但是,繼承的問題依然存在,基類的變化依然會傳給子類,所以,我們必須保證,基類中的非抽象部分是在很長一段時間內都不會發生變化的。
      策略模式的組成其實還包括一個環境角色類(Context),它擁有策略抽象的引用,可以隨時更換策略和執行策略。上面的例子中,抽象類似乎就是一個環境角色類,但真正決定策略的動態執行的方式是子類,子類才是真正的Context。環境角色類的真正意義是策略的具體應用環境,毫無疑問,就是具體的子類。
     策略模式還可以與委托掛鈎。
     還是上面的例子,我們這次建立一個委托類:
public class FunctionTest{
     private Plane mPlane;
     
     FunctionTest(Plane plane){
          this.mPlane = plane;
      }

     void functionTest(){
          plane.testFunction();
      }
}

public class test{
public static void main(String[] args){
RocketPlane plane = new RocketPlane(new FlyWithRocket(), new AttackWithFire());
FunctionTest test = new FunctionTest(plane);
test.functionTest();
}
}

      使用委托類到底有什么好處? 委托類提供的就是一個間接層,我們不需要知道有關於戰斗機的具體細節,我們只知道,使用functionTest()就可以讓我們的戰斗機飛起來,攻擊敵人。這就是封裝,客戶只知道調用委托類提供的方法就可以,就算戰斗機的內部構造發生變化,像是testFunction()變成function(),又和我們客戶代碼有什么關系呢?

      到了這里,策略模式的基本內容已經講完了,通過使用接口來實現對象組合,我們就可以充分的做到代碼復用。實現策略模式真的需要繼承嗎?不一定,因為策略模式只是為了封裝一組算法族,然后實現算法的替換而已,只要達到這個目的都可以說是策略模式。
       介紹完策略模式后,最后的部分就是針對我們上面提出的三個問題進行解答:
       1.使用策略模式的原因?
          原因與意圖一樣。有些書本可能會講,策略模式就是為了處理if...else if...else這種大量的條件語句塊,但千萬不要因為這樣的提示而隨便使用策略模式,雖然這里的確使用了算法的替換,但是,我們並不一定能完全去除掉這些條件判斷,肯定的是我們可以將這些代碼封裝起來,尤其是那些類似的算法中的重復代碼。但這並不是策略模式該做的事情,它更大的作用就是可以在運行時動態的改變算法,而且可以將算法的實現交給具體的子類實現,我們類的設計者就不需要為以后的變化考慮太多,只要為以后的變化留下改動的空間就可以了。
        2.策略模式的實現?
           策略模式的實現是各種各樣,但是基本的思想是不會變的,不同的語言,不同的情境都可以使用不同實現的策略模式。
        3.策略模式的優缺點?
           優點就是它的意圖,缺點呢?這個就難講了,因為大部分情況下模式的缺點都是不正確的使用模式,而不能歸於模式本身。策略模式最大的缺點,可能就是我們需要維護一大堆類,但這個問題可以利用工廠方法模式解決。
         最后就是貼出我們上面例子的UML圖,策略模式的UML圖大概就是這樣:
         
      

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM