1. 概述

在这篇文章中,我们将探讨如何在Java 8中实现策略模式。

首先,我们会概述这个设计模式,并解释在旧版本Java中如何传统地实现它。

接下来,我们将使用Java 8的lambda表达式再次尝试该模式,以减少代码的冗余。

2. 策略模式

策略模式本质上允许我们在运行时改变算法的行为。

通常,我们首先会定义一个接口,用于应用算法,然后针对每种可能的算法多次实现该接口。

假设我们有一个需求,根据购买是否是圣诞节、复活节或新年,对购买应用不同类型的折扣。首先,让我们创建一个Discounter接口,每个策略都将实现它:

public interface Discounter {
    BigDecimal applyDiscount(BigDecimal amount);
}

假设我们想在复活节时应用50%的折扣,在圣诞节时应用10%的折扣。那么,让我们为这些策略分别实现我们的接口:

public static class EasterDiscounter implements Discounter {
    @Override
    public BigDecimal applyDiscount(final BigDecimal amount) {
        return amount.multiply(BigDecimal.valueOf(0.5));
    }
}

public static class ChristmasDiscounter implements Discounter {
   @Override
   public BigDecimal applyDiscount(final BigDecimal amount) {
       return amount.multiply(BigDecimal.valueOf(0.9));
   }
}

最后,让我们在一个测试中尝试一种策略:

Discounter easterDiscounter = new EasterDiscounter();

BigDecimal discountedValue = easterDiscounter
  .applyDiscount(BigDecimal.valueOf(100));

assertThat(discountedValue)
  .isEqualByComparingTo(BigDecimal.valueOf(50));

这种方法工作得很好,但问题在于每次需要为每个策略创建一个具体类,这可能会有些繁琐。另一种选择是使用匿名内部类,但这仍然相当冗长,而且不如前一种解决方案方便。

3. 利用Java 8

自从Java 8发布以来,lambda表达式的引入使得匿名内部类型几乎变得多余。这意味着现在在线创建策略更加简洁且易于维护。

此外,函数式编程的声明式风格使我们能够实现以前无法实现的模式。

3.1. 减少代码冗余

让我们尝试创建一个线上的EasterDiscounter,这次使用lambda表达式:

Discounter easterDiscounter = amount -> amount.multiply(BigDecimal.valueOf(0.5));

如我们所见,我们的代码现在更清晰,更易于维护,与之前的效果相同,但只需要一行代码。基本上,lambda可以被视为匿名内部类型的替代品。

当我们想要在线声明更多的Discounters时,这种优势更为明显:

List<Discounter> discounters = newArrayList(
  amount -> amount.multiply(BigDecimal.valueOf(0.9)),
  amount -> amount.multiply(BigDecimal.valueOf(0.8)),
  amount -> amount.multiply(BigDecimal.valueOf(0.5))
);

当我们要定义大量Discounters时,我们可以一次性在同一个地方静态地声明它们。Java 8甚至允许我们在接口中定义静态方法。

因此,我们可以尝试在一个单独的类中创建所有的lambda表达式:

public interface Discounter {
    BigDecimal applyDiscount(BigDecimal amount);

    static Discounter christmasDiscounter() {
        return amount -> amount.multiply(BigDecimal.valueOf(0.9));
    }

    static Discounter newYearDiscounter() {
        return amount -> amount.multiply(BigDecimal.valueOf(0.8));
    }

    static Discounter easterDiscounter() {
        return amount -> amount.multiply(BigDecimal.valueOf(0.5));
    }
}

如你所见,我们用不多的代码实现了很多功能。

3.2. 利用函数组合

让我们修改Discounter接口,使其扩展UnaryOperator接口,并添加一个combine()方法:

public interface Discounter extends UnaryOperator<BigDecimal> {
    default Discounter combine(Discounter after) {
        return value -> after.apply(this.apply(value));
    }
}

实质上,我们正在重构Discounter,并利用一个事实:应用折扣是一个将BigDecimal实例转换为另一个BigDecimal实例的函数,这样我们就可以访问预定义的方法。由于UnaryOperator接口带有apply()方法,我们可以直接替换applyDiscount

combine()方法只是一个抽象,用于将this的结果应用到另一个Discounter上。它使用内置的函数式apply()来实现这一点。

现在,让我们尝试累计应用多个Discounters到一个金额上。我们将使用函数式reduce()combine()来实现:

Discounter combinedDiscounter = discounters
  .stream()
  .reduce(v -> v, Discounter::combine);

combinedDiscounter.apply(...);

请注意第一个reduce参数。如果没有提供折扣,我们需要返回不改变的值。这可以通过提供一个身份函数作为默认折扣器来实现。

这是一种有用且不太冗余的替代标准迭代的方式。考虑到我们从箱子里得到的函数组合方法,它还为我们提供了许多免费的功能。

4. 总结

在这篇文章中,我们解释了策略模式,并展示了如何使用lambda表达式以更简洁的方式实现它。

这些示例的实现可以在GitHub上找到。这是一个基于Maven的项目,所以可以直接运行。