【Java 设计模式】策略模式

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() {
// 618 折扣
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
// Lists.newArrayList 引入的包
// import com.google.common.collect.Lists;
// ----------------------------------------
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() 来做到这一点:

1

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

这是执行标准迭代的有用且不那么冗长的替代方法。如果我们考虑开箱即用的功能组合方法,它还免费为我们提供了更多功能。

4. 结论

在本文中,我们解释了策略模式,并演示了如何使用 lambda 表达式以不那么冗长的方式实现它。


【Java 设计模式】策略模式
https://cuifuan.github.io/2023/03/01/back-end/design-pattern/strategic-pattern/
作者
cuifuan
发布于
2023年3月1日
许可协议