策略模式---算法策略选择的“懒人用法”

      策略模式并没有表面上那么高深,相反,它的实现意图非常浅显,就是我们程序员实在不想在判断一大堆条件后写一些基本上都差不多的代码(即算法的变体,明明是同一种算法,只是因为输入不一样就要重复一次),烦!没错,就是这种感觉,催生这种模式。我相信,当初设计这种模式的家伙一定是忍无可忍了。

      为讲解策略模式,我特意准备一个活用策略模式的接口类---Collections.sort这个方法中的comparator。
       comparator使用了策略模式(有些人会有异议,认为这应该是模板方法,这里暂且搁置这些争议),基本上,我对于List元素的排序都是使用它,因为自己也做过测试,它的运行效率并不比任何排序算法差(其实它内部封装的就是快速排序),而且更加方便,有一个好用的封装接口对于程序员来说,是非常舒服的,当然,这样也会使我们更“懒”,我们应该追求更加精妙的设计,但是实用性必须是要正视的首要因素,无论什么设计,只要失去实用性这个特点,它就几乎不能使用。好吧,先上一个例子:
       
public class ComparatorList implements Comparator {
       @Override
       public int compare(Object arg0, Object arg1) {
             Integer obj0 =(Integer) arg0;
             Integer obj1 = (Integer)arg1;
             int flag  = obj0.compareTo(obj1);
             return - flag;
       }
}

我们先实现一个Comparator,这里比较的是数字的大小,按照数列从低到高的顺序排序(默认顺寻是从高到低,所以这里flag要取反)。我们可以看到,compare的方法参数是两个Object对象,这样就能比较任何类型的对象,就算是自定义对象(基本类型必须用包装类型),然后我们就能在这个方法中自定义自己的比较方法。

       接着就是使用这个类。
      
public void sort(List<Integer> list) {
      ComparatorList comparator = new ComparatorList();
      Collections.sort(list, comparator);
}

      这就是策略模式还有接口封装的使用。先说接口封装,我们定义一个接口,目的就是想要在所有实现类中定义一个通用规则,然后由用户自定义自己的方法,这样就能解决用户需求多样的问题。我们永远不知道用户的需要到底有多少,什么样的,所以,干脆将这个选择直接抛给用户而不是想当然的为他们设置限制(关于接口封装,以后会有专门介绍的)。继续回到我们的策略模式上来。如果我想要我的数列能够同时显示从高到低和从低到高的顺序,那么,我们可以继续实现一个Comparator---ComparatorList2,然后只要在我的代码中这样写:

ComparatorList2 comparator2 = new ComparatorList2(); 

Collections.sort(list, comparator2);

       就能达到我们上面的目的,以此类推,如果想要比较其他类型的List,可以继续实现Comparator类。于是,我们就可以不必先判断List的元素类型然后调用对应的方法,否则真的会没完没了!

      策略模式的原理是否很简单?如果还是有不清楚的,这里就提供一个简易的使用模型:

      
public interface Strategy {
//策略方法 //
    public abstract double method();
}
public class Strategy1 implements Strategy {
      @Override
      public void method() {
         System.out.println(this.getClass().getName() + "的方法");

} } public class Strategy2 implements Strategy { @Override
public void method() { System.out.println(this.getClass().getName() + "的方法"); } } public class Context { //上下文类 // private Strategy mstrategy; public Context(Strategy strategy) { this.mstrategy = strategy; } //策略方法 // public void method() { this.mstrategy.method(); } } 主类 public static void main(String[] args) { Context context = new Context(new Strategy1()); context.method();// 执行算法1 context = new Context(new Strategy2()); context.method();// 执行算法2 }

      这是典型的策略模式的使用方法。我们将策略类作参数,传递给Context类,然后由它来启动它们。这样有什么好处呢?为什么不是直接调用策略类而是将策略类交给Context类来启动呢?因为间接层能为我们带来更多的操作。如果这里是想要更改一下算法,那么,我只需要写个这样的方法:

pulic void set(Strategy strategy){
     mstrategy = strategy;
}

       就能将算法进行更改而不需要将没用的算法的代码去掉。这就是间接层的灵活性(因为使用间接层意味着逻辑分离),也是重构的手法中很多都是增加间接层的原因。当然,有个必须正视的问题,增加间接层除了带来使用上的灵活性之外,还带来效率上的问题,因为它具有额外的开销,一般情况下,这种考虑是不必要的,因为整个程序运行期间,主要影响效率的问题可能不是这些间接层带来的。

     使用策略模式的好处就在于Context类,因为它就是策略选择与策略实现的间接层。对于这个间接层,我们可以看到,最基本的就是它一定含有接口类中定义的通用算法,因为这是一种委托关系,将这个计算交给传进来的具体算法。但是有一个问题,如果我们的策略类需要Context类的数据呢?按照一般人的思维来讲,会想到两种方法:

1.将Context类引用作为策略类的参数。这种做法的好处就是当我们的Context添加新的公共功能时,我们的策略类能够马上使用它们,但是坏处非常明显,就是Context类的信息隐藏被破坏了,我需要考虑放松Context类不想被公开的数据和方法的访问权限,像是提供包访问权限。

2.在Context类的委托算法中将Context作为参数。坏处实在太明显了,任何策略类都会受到这个引用的干扰,明明只是一些特殊的策略类才需要用到,但是这种方法的耦合是最小的。
第一种方法是可以根据具体情况进行修改的,比如说,如果这只是某个特殊的算法才用到,我就只将这个引用传给它的构造器或者是初始化一个Context类(最好是选择初始化的方法,如果是构造器参数,那么,在Context类中就要特别声明这个特殊策略类),所以,最好是使用第一种方法。
     如果只是讨论单个接口的话,策略模式的好处与使用继承自抽象类的模板方法(这个以后会讲,基本就是将我们的接口换成抽象类)没有什么太大的区别,如果是多个接口类呢?比如说,我上面的代码可能需要多增加一个接口来产生我需要进行操作的数据,那么,我完全可以在我的Context中这样写:
     
 public int createInteger(CreateDate date){     //CreateDate接口
      int num = date.create();                    //CreateDate接口的create()方法
}

所以,使用策略模式还有一个好处,就是我的代码能够使用多个不同的通用接口,如果是模板方法,我们就难以进行这样的操作,这也是为什么相比抽象类的继承来说,我们更多使用接口的原因。

     策略模式之所以有这些好处,是因为它实现了策略选择逻辑(就是开头讲的那些条件判断语句)与策略实现逻辑的完全分离。逻辑分离的好处就是我们可以在完全不知道具体实现的情况下毫无副作用的使用它们,以及我们修改实现代码时,选择逻辑可以完全不用知道依然安全的使用。所以,追求封装,是我们面向对象编程中永远的主题。

      模式的讨论永远是没有尽头的,因为模式的讨论总是伴随着实践,而实践总是有着各种复杂的情况处理使得一种模式难以完全适应,但总体而言,只要用对模式,模式的好处还是非常显著的,前提还是,我们必须用对模式。
posted @ 2012-10-26 21:22  文酱  阅读(2651)  评论(2编辑  收藏  举报