设计模式(二):策略模式

概述

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

策略模式旨在解决不同逻辑下相同的对象执行不同策略的问题。

当我们遇到同一个方法,里面会根据需要多个逻辑的分支,分支里的行为都不同,但是都服务于同一个功能,这个时候就可以使用策略模式,将行为抽象为一个策略接口中的抽象方法,由接口的实现类——也就是策略类——去实现各中具体的行为。

策略模式也是一种比较常见且好用的设计模式,线程池的拒绝策略就使用了策略模式。

一、简单实现

简单的拿一个根据情况需要导出不同文件的接口举例:

1
2
3
4
5
6
7
8
9
10
11
public void exportFile(String type){
if (type == "excel") {
// 导出excel
} else if (type == "word") {
// 导出word
} else if (type == "pdf") {
// 导出pdf
}else {
throw new RuntimeException("错误的文件类型!");
}
}

这些分支里目的都是导出文件,但是各自有各的实现代码。换而言之,这些不同逻辑分支下的代码只有行为是不同的。现在我们将导出方法抽象成为一个策略接口中的抽象方法,将每个逻辑分支的处理代码都抽成实现策略接口的各个策略类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 文件导出接口
public interface IExportFile(){
void export();
}

// 实现类
public class ExportExcel implements IExportFile{
@Override
public void export() {
// 导出excel
}
}
public class ExportWord implements IExportFile{
@Override
public void export() {
// 导出word
}
}
public class ExportPdf implements IExportFile{
@Override
public void export() {
// 导出pdf
}
}

然后调用的时候直接将接口实现类作为参数传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 改造原有方法,将策略接口的实现作为参数传入
public void exportFile(String type){
IExportFile exportFile;
if (type == "excel") {
exportFile = new ExportExcel();
} else if (type == "word") {
exportFile = new ExportWord();
} else if (type == "pdf") {
exportFile = new ExportPdf();
}else {
throw new RuntimeException("错误的文件类型!");
}
exportFile.export();
}

我们可以看到,原本耦合在调用方法里的行为被解放出来了,通过为策略接口更换策略类,我们可以很方便的切换行为。

策略模式改进原有方法

二、策略池与上下文对象

策略池

根据上文的简单例子,我们将具体的策略通过策略接口与调用方法耦合了,但是我们不难发现,现在的实现仍然需要通过大量的 if-else 判断去选择执行策略

实际上,我们可以这么考虑,代码被封装到实现类里以后,实际上一个策略跟对应的判断条件实际上就是一种 key 和 value 之间的映射关系了,我们可以根据这个思路,换一个更简洁一些的方式去替换 if-else 的代码:比如将条件字段与策略类放入 Map 集合实现的一个策略池中,直接通过 key 去获取对应的策略类

还是基于上述导出接口的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 策略池
static Map<String,IExportFile> exportStrategyPool = new HashMap<>();
static {
exportStrategyPool.put("excel", new ExportExcel());
exportStrategyPool.put("word", new ExportWord());
exportStrategyPool.put("pdf", new ExportPdf());
}

// 导出文件
public void exportFile(String type){
if (!exportStrategyPool.containsKey(type)) {
throw new RuntimeException("错误的文件类型!");
}
IExportFile exportFile = exportStrategyPool.get(type);
exportFile.export();
}

我们可以看到,通过策略池,我们直接简化了大量的 if-else 代码。

实际上,考虑到实现类是无状态的,那么策略类和策略池都应该是单例的,因此,这里使用了饿汉式去创建策略池,这里同样有许多优化的地方:比如可以手动创建改为通过反射自动装填策略类;可以创建枚举类或者将条件作为常量来规范策略的对应关系;如果在 spring 项目中,也可以考虑通过 spring 去创建......

这些的都可以根据需求进行优化的,但是核心仍然是在调用前建立条件与策略的映射关系

策略池的实现

上下文对象

现在,出于优化 if-else 的原因,我们为导出方法加入了策略池,但是这个类的其他方法未必用得到,为此我们不妨将整个策略池和导出方法都封装到另一个单独的类里,只提供一个带条件参数的方法。现在策略池归上下文对象管理了,那么这个上下文对象也应该是单例的,就个人观点,单独放到一个独立的 Service 和对应一个实现类,由 spring 管理应该是比较合适的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// FileExporter 屏蔽了方法的具体实现
public class FileExporter {

private Map<String,IExportFile> exportStrategyPool;

public FileExporter() {
exportStrategyPool = new HashMap<>();

exportStrategyPool.put("excel", FileExporter::exportExcel);
exportStrategyPool.put("word", FileExporter::exportWord);
exportStrategyPool.put("pdf", FileExporter::exportPdf);
}

// 导出文件
public void exportFile(String type){
if (!exportStrategyPool.containsKey(type)) {
throw new RuntimeException("错误的文件类型!");
}
IExportFile exportFile = exportStrategyPool.get(type);
exportFile.export();
}
}

// 调用
new FileExporter().exportFile("word");

通过 FileExporter 这个承上启下的上下文对象,我们屏蔽了具体代码的实现,同时通过“人”去调用“行为”这个逻辑也更符合面向对象的思想。

上下文对象

三、配合函数式接口使用

策略模式+策略池的手段已经可以解决传统 if-else 的大多数问题了,但是他也随之带来了两个问题:

  • 一个方法要用到的策略会产生大量实现类;
  • 业务逻辑被分散到了各个实现类,无法方便的总览。

为此,JDK8 的函数式接口刚好能非常完美的解决这些痛点。

关于函数式接口,我已在JDK1.8新特性(三):Lambda表达式里介绍过了,这里就不再赘述,直接上手。

还是以上文的文件导出接口为例:

当我们完成这个功能以后,我们会在原来的基础上多处一个上下文对象,一个策略接口,以及 n 多个实现接口的策略类,一个策略对应一个策略实现类的问题是导致类数量膨胀的原因,因此我们可以将策略接口替换为函数式接口,这样就可以在需要的时直接通过 Lambda 表达式传入实现类,避免新建类

1
2
3
4
5
6
7
8
9
10
11
12
13
// 将原本的IExportFile改为函数式接口
@FunctionalInterface
public interface IExportFile {
void export();
}

// 在策略池阶段直接放入匿名实现类
static Map<String,IExportFile> exportStrategyPool = new HashMap<>();
static {
exportStrategyPool.put("excel", () -> System.out.println("excel"));
exportStrategyPool.put("word", () -> System.out.println("excel"));
exportStrategyPool.put("pdf", () -> System.out.println("excel"));
}

当然,实际工作中的方法肯定比区区一句 System.out.println()要复杂的多,我们不可能集中在策略池里写实现。这个问题也很好解决,我们可以将具体的方法抽出来放到一个实现类里,比 service 的实现类,或者上下文对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class FileExporter {

private Map<String,IExportFile> exportStrategyPool;

private FileExporter() {
exportStrategyPool = new HashMap<>();

exportStrategyPool.put("excel", this::exportExcel);
exportStrategyPool.put("word", this::exportWord);
exportStrategyPool.put("pdf", this::exportPdf);
}

// 导出文件
public void exportFile(String type){
if (!exportStrategyPool.containsKey(type)) {
throw new RuntimeException("错误的文件类型!");
}
IExportFile exportFile = exportStrategyPool.get(type);
exportFile.export();
}

// 导出的策略
public void exportExcel() {
System.out.println("导出excel");
}
public void exportWord() {
System.out.println("导出word");
}
public void exportPdf() {
System.out.println("导出pdf");
}
}

现在,业务代码可以都放在一个类里面了,总览起来也非常方便。

四、总结

通过策略模式,我们可以做到:

  1. 通过将行为抽象为一个策略接口,具体的行为作为接口的实现类,来分离方法和逻辑分支中的代码;
  2. 通过策略池来避免大量的 if-else 判断;
  3. 通过将策略池和方法封装到上下文对象来对外部屏蔽底层的实现;

对于策略模式带来的策略类过多,业务逻辑分散的问题:

  1. 将策略接口改为函数式接口,省去创建实现类,直接通过 Lambda 表达式直接传入匿名实现类;
  2. 在上述基础上,将实现方法统一写在一个类里,策略池在创建时通过 Lambda 表达式把类中的方法传入策略池。
0%