策略模式动态更改算法

      策略模式是设计模式中很重要的一种,它的主要意图就是:定义了算法族,分别封装起来,让它们之间可以互相替换。它让算法的变化可以独立于使用算法的客户,使得我们可以随时更改它们而不影响客户端的代码,而客户端可以自由选择不同的算法执行。

       要想了解策略模式,我们就要理解几个重要概念:

      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图大概就是这样:
         
      
原文地址:https://www.cnblogs.com/wenjiang/p/2937623.html