1.2.5 策略模式(5.9)

【最后编辑时间:2020.11】

【实验2:框架设计者】中,SortTest→IntSort结构——不管它们是在应用程序中还是在框架中,体现了一种设计模式——策略模式。

所谓策略/strategy或政策/policy指做事情的方式。处理同一件事情可以有不同的策略,例如外出旅游时,可以在多种不同的出行方式中选择,如坐汽车/火车/飞机、骑自行车或步行;海关对不同的商品按照不同的税率征收关税等等。正所谓“杀猪杀屁股,各有各的刀法”,面向对象语言通过多态来实现具体策略。

★策略模式,以策略的类层次定义和封装算法集合,环境类针对抽象策略编程。

1.环境类

某种程度上,只要设计一个Java 的抽象类型,就可以说使用了策略模式,因为该抽象类型总会存在用户,而该用户类型即环境类。如果将抽象类型IServer视为策略的类层次,那么面向对象领域最常见的Client→IServer结构,即应用了策略模式,策略模式的运用无处不在。其中,Client是环境类,可以有诸多的环境类重用策略类层次;IServer是抽象策略(角色),而IServer的子类型就是具体策略。策略模式结构图如图1-7所示。

策略模式中的之所以提及环境类,是为了强调单一职责原则的重要。例如SortTest→IntSort结构中,如果将策略IntSort的抽象方法封装到环境类SortTest中,将使得SortTest既包括测试代码又包括算法代码,而在实现算法时需要编写的是环境类的类层次。GoF的[5.9·8效果2一个代替继承的方法]的正确解读是:设计一个策略的类层次,来代替直接编写环境类的类层次,或“一个代替编写环境类的类层次的手段”。单纯地说“代替继承的手段”,该手段对于面向对象的程序员而言,简直是不可想象的神奇秘技。

策略模式仅仅简单地使用了多态技术,因此策略模式的优缺点都归结于多态。

  • 环境类针对抽象策略编程,因此算法可以有新的变体。程序员可以自定义新的策略实现。(类的扩展性)。
  • 用户可以自由切换具体策略,因为环境类不关心有哪些具体策略。参见[第三章创建对象]。
  • 多态本身就是根据具体对象的不同执行不同的行为,因此在面向对象程序中,分支结构通常较少使用,因为很多时候程序员自然地选择了多态。面向对象程序员在遇到少量需求变化时,可以会通过分支结构简单应付;如果需求变化较多时,通常会以多态重构其分支结构。

2. 以多态重构分支结构

 从重构分支结构的角度看,策略模式与[2.1.3工厂方法模式(3.3)]和[4.2状态模式(5.8)]是三胞胎

以多态重构分支结构是一个常用技巧。例如Context中需要计算比赛中选手的得分,给出该选手所得分数的平均数,于是程序员编写了sum1();当需要去掉一个最高和一个最低分,再计算平均数时,程序员可能给出sum2(),并在公开的接口sum()中以分支结构判断将执行何种算法。

在某些黑箱操作场合,选手得分要计算关键人士(key)给出的分数或关键人士们给出分数的平均数。事不过三,面向对象程序员此时通常会以多态重构其分支结构。如果继续使用分支结构则是糟糕的设计。

package method.strategy;
import tool.God;
import static tool.Print.*;
/**
 * 环境类通常是系统中有价值的类。
 * @author yqj2065
 * @version 0.1
 */
public class Context_bad{
    //其他有意义的方法
    分支结构///
    public double sum(int  s/*elect*/,double[] array,int... key){
        if(s == 0) return m1(array);
        if(s == 1) return m2(array);
        if(s == 2) return m3(array,key);
        return 0;
    }
    /**平均分*/
    private double sum1(double[] array){
        double sum=0;
        for(double x: array){
            sum+=x;
        }
        return sum/array.length;
    }
    /**去掉一个最高和一个最低分*/
    private double sum2(double[] array){
        double sum=0,max=0,min=array[0];
        for(double x: array){
            if(x>max)max=x;
            if(x<min)min=x;
            sum+=x;
        }
        return (sum-max-min)/(array.length-2);
}
/**黑箱操作*/
    private double sum3(double[] array,int... key){
        double sum=0;
        for(int i:key){
            sum += array[i];
        }
        return sum/ key.length;
    } 
}

重构的目标是将分支结构的每一个分支,放在一个子类的@Override函数中,而父类型的抽象方法,由包含分支结构的函数如sum承担(一般sum有参数如s用于分支判断,则删除该参数)。

重构过程如下:

①从包含分支结构的函数如sum获得抽象方法,并封装到抽象类型中。设计抽象策略如下:

public interface Sum{

    double sum(double[] array,int... key);

}

抽象方法sum(double[] array,int... key),事实上是对sum1、sum2、sum3函数的抽象,因此即使公开接口的函数名为foo而非sum,提取的抽象方法仍然应该取名sum;需要注意,sum的参数列表是具体的策略类所需数据的最大集合。最大集合意味着宁可多不可少——某些具体策略类如Sum1可以不使用参数列表中提供的某些数据如key,但是不可因为参数少而导致其他具体策略类缺乏需要的数据。GoF的[5.9·8效果6通信开销、9实现1]讨论了相关细节。

②各子类型封装不同算法。设计中,通常要考虑将子类型中的公共代码提取到父类型,例如SortTest→IntSort中的策略类包含swap()工具方法。而在本例中,分支结构的每一个分支都调用一个独立的函数。如果遇到复杂的分支语句,则需要先进行这样的预处理——每一个分支都调用一个独立的函数。

③Context中,以抽象类型作为方法调用的参数,或作为成员变量,它替代原来的(包含分支结构的函数如sum的)参数s。

package method.strategy;
public interface Sum{    
     double sum(double[] array,int... key);
}

package method.strategy;
public class Sum1 implements Sum{//平均分
    public double sum(double[] array,int... key){ //注意:没有使用参数列表提供的数据key
        double sum=0;
        for(double x: array){
            sum+=x;
        }
        return sum/array.length;
    }
}

package method.strategy;
import static tool.Print.*;
public class Context2{
    public static void printSum(Sum s,int... key){
        double[] array ={ 8,9,9,9,9,10,5};
        pln(s.sum(array,key));        
    }

    public static void test(){
        printSum(new Sum1(),0);
        printSum(new Sum2(),0);
        printSum(new Sum3(),0);
        printSum(new Sum3(),0,1);
        printSum((double[] array,int... key)->{//假设没有单独的不能上台面的Sum3类
                double sum=0;
                for(int i:key){
                    sum += array[i];
                }
                return sum/ key.length;
            },0,1);
    } 
}

如果让命令编程范式的C程序员编写这个程序,如果他僵化地仅仅考虑“解决一个问题所需要的算法是什么?”,他可能会沿用分支结构;但是,有经验的命令编程范式程序员,仍然会考虑函数/系统的组织问题,他会简单地用函数指针来完成算法的挑选和替换。

按照[2.2.2策略模式的扩展],策略模式包括①环境类多次使用策略模式或称多重策略,和②将策略转化为参数(行为参数化)。

 


下面为遗留文字。

[我觉得写到这里就可以了,除了环境类值得提一下外,策略模式=多态。yqj2065将学习和理解策略模式的难度系数设为0)]

 

  • 算法可以有新的变体。程序员可以自定义新的策略实现。(类的扩展性)
  • 用户可以自由切换具体策略。
  • 替代显式的分支语句。传统的多重条件判断不容易维护,需要重构。

 

 

3.策略类的设计

 

策略模式中,策略类是设计为抽象类、接口还是函数接口?

在Java8之前,策略类如果需要提供子类共享的方法,如IntSort提供swap()则设计为抽象类;或者都设计为接口。

Java 8之后,如果可能尽量将策略类设计为函数接口。重构例程3-4

策略类Sum的抽象方法sum()的参数列表问题:假定为了体现公平公正公开,sum()的参数列表只能够为:double sum(double[] array);但是,程序员又必须满足某些人的要求,让Sum类获得Context的某些数据(如int... key),怎么办?
可以让Sum与Context双向依赖——Sum有成员变量Context或局部变量Context (Context通过参数传递给Sum);相互依赖通常是糟糕的选择。
如果Sum属于类库而Context是应用,则只能够在类库中设计IContext,IContext定义回调接口int[] getKey()。

 

 

糟糕之处在于:

 

[GoF] 关于策略模式的描述为:『定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化』。

[算法,描述处理过程的步骤。一个简单的算法,可以用一个Java方法实现,也可以加上若干个private方法(以功能分解为目的)。

写策略模式,我觉得,要给读者一个这样的感觉:“你知道的!” 或者说,不需要我说太多,你就应该知道。]


  • 在需要增加新分支的时候,该代码不遵循OCP。可以通过设计类层次重构分支结构。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值