1.前言
在此篇文章,我们将看看如何在 Java 8 中实现策略设计模式。
首先,我们将概述该模式,并解释传统上它是如何在旧版本的 Java 中实现的。
接下来,我们将再次尝试该模式,只是这次使用 Java 8 lambda,减少了代码的冗长。
2.策略模式
本质上,策略模式允许我们在运行时改变算法的行为。
通常,我们会从一个用于应用算法的接口开始,然后为每个可能的算法多次实现它。
假设我们需要根据是 618、双十一还是新年,对购买应用不同类型的折扣。
首先,让我们创建一个 Discounter
接口,它将由我们的每个策略实现:
1 2 3
| public interface Discounter { BigDecimal applyDiscount(BigDecimal amount); }
|
然后假设我们想在 618
应用 20%
的折扣,在 双十一
应用 30%
的折扣。
让我们为这些策略中的每一个实现我们的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public static class MidYearDiscounter implements Discounter { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.8)); } }
public static class OneSDayDiscounter implements Discounter { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.7)); } }
|
最后,让我们在测试中尝试一个策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import com.demo.design.Discounter; import com.demo.design.impl.MidYearDiscounter; import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.Test; import java.math.BigDecimal;
public class TestMain { @Test public void testDiscounter() { Discounter midYearDiscounter = new MidYearDiscounter(); BigDecimal discountedValue = midYearDiscounter .applyDiscount(BigDecimal.valueOf(100)); AssertionsForClassTypes.assertThat(discountedValue) .isEqualByComparingTo(BigDecimal.valueOf(80)); } }
|
测试通过,也搞定了我们的问题,但另一些问题是必须为每个策略创建一个具体的类可能有点痛苦。
另一种方法是使用匿名内部类型,虽然仍然非常冗长,但是比以之前的解决方案更轻便一些:
1 2 3 4 5 6
| Discounter midYearDiscounter = new Discounter() { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.8)); } };
|
3.使用 Java 8
自 Java 8 发布以来,lambdas 的引入使匿名内部类型或多或少变得多余,这意味着创建策略现在变得更加简洁和容易。
3.1 减少代码冗长
让我们尝试创建一个内联 MidYearDiscounter
,只是这次使用 lambda 表达式:
1
| Discounter midYearDiscounter = amount -> amount.multiply(BigDecimal.valueOf(0.8));
|
正如我们所看到的,我们的代码现在变得更简洁、更易于维护,实现了与以前相同的效果,但只需一行代码。
本质上,可以将 lambda 视为匿名内部类型的替代品。
当我们想要在行中声明更多的 Discounter 时,这种优势变得更加明显:
1 2 3 4 5 6 7 8
|
List<Discounter> discounters = Lists.newArrayList( amount -> amount.multiply(BigDecimal.valueOf(0.8)), amount -> amount.multiply(BigDecimal.valueOf(0.9)), amount -> amount.multiply(BigDecimal.valueOf(0.5)) );
|
当我们想要定义很多 Discounter
时,我们可以在一个地方静态地声明它们。
如果我们愿意,Java 8 甚至允许我们在接口中定义静态方法。
因此,与其在具体类或匿名内部类型之间进行选择,不如尝试在单个类中创建 lambda:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public interface Discounter { BigDecimal applyDiscount(BigDecimal amount);
static Discounter midYearDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.8)); }
static Discounter newYearDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.5)); }
static Discounter oneSDayDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.7)); } }
|
正如我们所看到的,我们可以使用很少的代码进行实现。
3.2 利用函数组合
让我们基于 Discounter
接口新建一个 DiscounterPlus
接口,使其扩展 UnaryOperator
接口,然后添加 combine()
方法:
1 2 3 4 5 6 7 8 9 10
| import java.math.BigDecimal; import java.util.function.UnaryOperator;
public interface DiscounterPlus extends UnaryOperator<BigDecimal> {
default DiscounterPlus combine(DiscounterPlus after) { return value -> after.apply(this.apply(value)); }
}
|
本质上,我们正在重构我们的 Discounter
并利用一个事实,即应用折扣是将 BigDecimal
实例转换为另一个 BigDecimal
实例的函数,允许我们访问预定义的方法。由于 UnaryOperator
带有一个 apply()
方法,我们可以用它替换 applyDiscount
。
combine()
方法只是将一个 Discounter
应用于 this
的结果的一种抽象。它使用内置函数 apply()
来实现这一点。
现在,让我们尝试将多个折扣累积应用于一个金额。我们将通过使用函数 reduce()
和 combine()
来做到这一点:
请特别注意第一个 reduce
参数。当没有提供折扣时,我们需要返回不变的值。这可以通过提供一个身份函数作为默认折扣商来实现。
这是执行标准迭代的有用且不那么冗长的替代方法。如果我们考虑开箱即用的功能组合方法,它还免费为我们提供了更多功能。
4. 结论
在本文中,我们解释了策略模式,并演示了如何使用 lambda 表达式以不那么冗长的方式实现它。