策略


定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

策略模式:Strategy,是指,定义一组算法,并把其封装到一个对象中。然后在运行时,可以灵活的使用其中的一个算法。

策略模式在Java标准库中应用非常广泛,我们以排序为例,看看如何通过Arrays.sort()实现忽略大小写排序:

策略 - 图1

如果我们想忽略大小写排序,就传入String::compareToIgnoreCase,如果我们想倒序排序,就传入(s1, s2) -> -s1.compareTo(s2),这个比较两个元素大小的算法就是策略。

我们观察Arrays.sort(T[] a, Comparator<? super T> c)这个排序方法,它在内部实现了TimSort排序,但是,排序算法在比较两个元素大小的时候,需要借助我们传入的Comparator对象,才能完成比较。因此,这里的策略是指比较两个元素大小的策略,可以是忽略大小写比较,可以是倒序比较,也可以根据字符串长度比较。

因此,上述排序使用到了策略模式,它实际上指,在一个方法中,流程是确定的,但是,某些关键步骤的算法依赖调用方传入的策略,这样,传入不同的策略,即可获得不同的结果,大大增强了系统的灵活性。

如果我们自己实现策略模式的排序,用冒泡法编写如下:

策略 - 图2

一个完整的策略模式要定义策略以及使用策略的上下文。我们以购物车结算为例,假设网站针对普通会员、Prime会员有不同的折扣,同时活动期间还有一个满100减20的活动,这些就可以作为策略实现。先定义打折策略接口:

  1. public interface DiscountStrategy {
  2. // 计算折扣额度:
  3. BigDecimal getDiscount(BigDecimal total);
  4. }

接下来,就是实现各种策略。普通用户策略如下:

  1. public class UserDiscountStrategy implements DiscountStrategy {
  2. public BigDecimal getDiscount(BigDecimal total) {
  3. // 普通会员打九折:
  4. return total.multiply(new BigDecimal("0.1")).setScale(2, RoundingMode.DOWN);
  5. }
  6. }

满减策略如下:

  1. public class OverDiscountStrategy implements DiscountStrategy {
  2. public BigDecimal getDiscount(BigDecimal total) {
  3. // 满100减20优惠:
  4. return total.compareTo(BigDecimal.valueOf(100)) >= 0 ? BigDecimal.valueOf(20) : BigDecimal.ZERO;
  5. }
  6. }

最后,要应用策略,我们需要一个DiscountContext

  1. public class DiscountContext {
  2. // 持有某个策略:
  3. private DiscountStrategy strategy = new UserDiscountStrategy();
  4. // 允许客户端设置新策略:
  5. public void setStrategy(DiscountStrategy strategy) {
  6. this.strategy = strategy;
  7. }
  8. public BigDecimal calculatePrice(BigDecimal total) {
  9. return total.subtract(this.strategy.getDiscount(total)).setScale(2);
  10. }
  11. }

调用方必须首先创建一个DiscountContext,并指定一个策略(或者使用默认策略),即可获得折扣后的价格:

  1. DiscountContext ctx = new DiscountContext();
  2. // 默认使用普通会员折扣:
  3. BigDecimal pay1 = ctx.calculatePrice(BigDecimal.valueOf(105));
  4. System.out.println(pay1);
  5. // 使用满减折扣:
  6. ctx.setStrategy(new OverDiscountStrategy());
  7. BigDecimal pay2 = ctx.calculatePrice(BigDecimal.valueOf(105));
  8. System.out.println(pay2);
  9. // 使用Prime会员折扣:
  10. ctx.setStrategy(new PrimeDiscountStrategy());
  11. BigDecimal pay3 = ctx.calculatePrice(BigDecimal.valueOf(105));
  12. System.out.println(pay3);

上述完整的策略模式如下图所示:

  1. ┌───────────────┐ ┌─────────────────┐
  2. DiscountContext│─ ─>│DiscountStrategy
  3. └───────────────┘ └─────────────────┘
  4. ┌─────────────────────┐
  5. ├─│UserDiscountStrategy
  6. └─────────────────────┘
  7. ┌─────────────────────┐
  8. ├─│PrimeDiscountStrategy
  9. └─────────────────────┘
  10. ┌─────────────────────┐
  11. └─│OverDiscountStrategy
  12. └─────────────────────┘

策略模式的核心思想是在一个计算方法中把容易变化的算法抽出来作为“策略”参数传进去,从而使得新增策略不必修改原有逻辑。

练习

使用策略模式新增一种策略,允许在满100减20的基础上对Prime会员再打七折。

策略 - 图3下载练习:策略模式练习 (推荐使用IDE练习插件快速下载)

小结

策略模式是为了允许调用方选择一个算法,从而通过不同策略实现不同的计算结果。

通过扩展策略,不必修改主逻辑,即可获得新策略的结果。

读后有收获可以支付宝请作者喝咖啡:

策略 - 图4