1 解决的问题

一天, 你打算为游客们创建一款导游程序。 该程序的核心功能是提供美观的地图, 以帮助用户在任何城市中快速定位。

用户期待的程序新功能是自动路线规划: 他们希望输入地址后就能在地图上看到前往目的地的最快路线。

程序的首个版本只能规划公路路线。 驾车旅行的人们对此非常满意。 但很显然, 并非所有人都会在度假时开车。 因此你在下次更新时添加了规划步行路线的功能。 此后, 你又添加了规划公共交通路线的功能。

而这只是个开始。 不久后, 你又要为骑行者规划路线。 又过了一段时间, 你又要为游览城市中的所有景点规划路线。

导游代码将变得非常臃肿

尽管从商业角度来看, 这款应用非常成功, 但其技术部分却让你非常头疼: 每次添加新的路线规划算法后, 导游应用中主要类的体积就会增加一倍。 终于在某个时候, 你觉得自己没法继续维护这堆代码了。

无论是修复简单缺陷还是微调街道权重, 对某个算法进行任何修改都会影响整个类, 从而增加在已有正常运行代码中引入错误的风险。

此外,团队合作将变得低效。如果你在应用成功发布后招募了团队成员,他们会抱怨在合并冲突的工作上花费了太多时间。在实现新功能的过程中,你的团队需要修改同一个巨大的类,这样他们所编写的代码相互之间就可能会出现冲突。

2 策略模式

将不同的策略 or 算法独自封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。这里的算法,相当于平时的业务逻辑。

3 实现代码

抽象策略:实现两个数字之间的运算。
具体策略:相加运算、相减运算、相乘运算。
客户端:创建一个特定策略对象并传递给上下文。上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。
上下文:维护具体策略的引用,并通过策略接口与该对象进行交流。

先是 策略的定义

// 计算策略接口
public interface ICompute {
    String compute(int first, int second);
}
// 具体策略
public class AddCompute implements ICompute {
    @Override public String compute(int first, int second) {
        return "计算:" + first + " + " + second + " = " + (first + second);
    }
}
public class SubCompute implements ICompute {
    @Override public String compute(int first, int second) {
        return "计算:" + first + " - " + second + " = " + (first - second);
    }
}
public class MulCompute implements ICompute {
    @Override public String compute(int first, int second) {
        return "计算:" + first + " * " + second + " = " + (first * second);
    }
}

然后是 策略的创建和使用

public class Context {
    private ICompute compute;
    public Context() { this.compute = new AddCompute(); }
    public void setCompute(ICompute compute) { this.compute = compute; }
    public void calc(int first, int second) {
        System.out.println(compute.compute(first, second));
    }
}
 
// 测试用例
public class TestCompute {
    public static void main(String[] args) {
        Context context = new Context();
        context.setCompute(new AddCompute());
        context.calc(4, 2);
        context.setCompute(new SubCompute());
        context.calc(4, 2);
        context.setCompute(new MulCompute());
        context.calc(4, 2);
    }
}

但,这其实没有发挥策略模式的优势,而是退化成了:面向对象的多态或基于接口而非实现编程,非动态,直接在代码中指定了使用哪种策略。

而实际开发中场景更多的是:事先并不知道会使用那种策略,而是在程序运行期间,根据配置、用户输入、计算记过等不确定因素,动态地决定使用那种策略。对于上述这种 无状态的(不包含成员变量,只是纯粹的算法实现),策略对象可以共享的场景,可以把 Context 改写为工厂类的实现方式:

public class Context {
    private static final Map<String, ICompute> computes = new HashMap<>();
 
    static {
        computes.put("+", new AddCompute());
        computes.put("-", new SubCompute());
        computes.put("*", new MulCompute());
        computes.put("/", new DivCompute());
    }
 
    public void calc(String operator, int first, int second) {
        System.out.println(computes.get(operator).compute(first, second));
    }
}
 
// 修改后的测试用例
public class TestCompute {
    public static void main(String[] args) {
        Context context = new Context();
        context.calc("+", 4, 2);
        context.calc("-", 4, 2);
        context.calc("*", 4, 2);
        context.calc("/", 4, 2);
    }
}

4 优缺点

优点

  • 动态切换对象内的算法;
  • 将算法的实现与使用算法的代码隔离开;
  • 开闭原则。

缺点

  • 客户端必须知晓策略间的不同—他需要选择合适的策略;
  • 小型的策略,不如函数式编程简洁 (匿名函数实现不同版本算法);
  • 许多现代编程语言支持函数类型功能,允许你在一组匿名函数中实现不同版本的算法。这样,你使用这些函数的方式就和使用策略对象时完全相同,无需借助额外的类和接口来保持代码简洁。

5 应用场景

  • 系统需要动态地切换几种算法;
  • 多重条件选择语句,想对分支判断进行隐藏,可用策略模式把行为转移到具体策略类中;
  • 只希望客户端直接使用已封装好算法,而不用关心算法的具体实现细节;

6 经典应用例子

Java 8 开始支持 lambda 方法, 它可作为一种替代策略模式的简单方式。

这里有一些核心 Java 程序库中策略模式的示例: