封面来源:策略设计模式 (refactoringguru.cn),如有侵权,请联系删除。

Java 设计模式学习网站:Java设计模式:23种设计模式全面解析(超级详细)

菜鸟教程:策略模式

Bilibili 视频:尚硅谷Java设计模式(图解+框架源码剖析)

1. 模式的定义与特点

与建造者模式的讲解一样,先来一些“看不懂”的定义与概念:

参考链接:策略模式(策略设计模式)详解

策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

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

这种算法体现了几个设计原则:

1、把变化的代码从不变的代码中分离出来(行为和对象分离);

2、针对接口编程而不是具体类;

3、多用组合 / 聚合,少用继承。

优点

1、多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句,如 if…else 语句、switch…case 语句。

2、策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。

3、策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。

4、策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。

5、策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。

缺点

1、客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。

2、策略模式造成很多的策略类,使程序更加复杂,增加维护难度。

3、所有策略类都需要对外暴露。

需要注意的是: 每添加一个策略就要增加一个类,当策略过多就会导致类数目庞大。如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。

2. 适用场景

参考链接:策略模式(策略设计模式)详解

在程序设计中,通常在以下几种情况中使用策略模式较多:

1、一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。

2、一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。

3、系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。

4、系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。

5、多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

叩击灵魂的三问:

1、策略模式解决了哪些问题?

2、为什么要使用策略模式?

3、怎么使用策略模式?

3. 简单应用

3.1 问题的出现

假设我是一个商店的老板,为了获取更多的利润,决定举办一次促销活动,商店的货品全场 8 折。那么怎么用代码计算现在商品的价格呢?

简单!乘以 0.8 不就完了吗?

好,暂时解决问题。

随着促销活动的进行,我发现八折的折扣有点太高了,对折扣进行紧急修改,改成 9 折。那这个时候又该怎么用代码计算商品的价格呢?

也简单啊,乘以 0.9 呗。

虽然是可以这么解决,但是这样修改代码并不是一个好方案。

因为直接修改原算法很有可能会造成一些莫名的错误,并且其他地方可能依赖了原算法,一旦修改,其他地方也将产生错误。

而且我是个奸商,我想获取更多的利润,又把折扣改成 9.5 折,这个时候再去修改算法的话,代码产生错误的概率又会增大。

随着我的生意越做越大,促销活动越来越频繁,一会全场 9 折,一会全场 8 折,一会满 300 减 50,难道每次促销都去修改计算商品价格的算法吗?

显然,这样是行不通的。

3.2 问题的解决

那么应该怎么做呢?

如果有一个像诸葛亮一样的军师,给我们一些锦囊就好了,面对不同的情况只需要打开不同的锦囊就完事了。

诶~

根据这个思想,我们可以将各种计算商品价格的算法封装起来,当我需要某种算法时,进行选择不就完事了?

而且这些算法都是为了计算商品价格的,很显然可以使用一个接口,将这种行为抽象出来,让后续编写的计算商品价格的算法实现这个接口。

为了能够使用这些算法,我们只需要在需要使用算法的类中添加接口类型的成员变量,并在构造函数中使用,以便使用时可以传入具体的算法。这个类起承上启下的作用,屏蔽高层模块对策略、算法的直接访问,我们一般称这个类为 Context 上下文,也叫 Context 封装角色。

有点懵?没有关系,我们看看代码的实现!

3.3 代码的实现

先定义一个促销接口,内部有一个抽象的促销方法:

1
2
3
4
5
6
7
8
9
/**
* @author mofan 2021/2/27
*/
public interface Promotion {
/**
* 促销方法的实现
*/
void promote();
}

然后我们编写促销方法的具体实现类,编写不同的促销方案。

促销方案 A:

1
2
3
4
5
6
7
8
9
/**
* @author mofan 2021/2/27
*/
public class PromotionPlanA implements Promotion {
@Override
public void promote() {
System.out.println("促销方案 A");
}
}

促销方案 B:

1
2
3
4
5
6
7
8
9
/**
* @author mofan 2021/2/27
*/
public class PromotionPlanB implements Promotion {
@Override
public void promote() {
System.out.println("促销方案 B");
}
}

促销方案 C:

1
2
3
4
5
6
7
8
9
/**
* @author mofan 2021/2/27
*/
public class PromotionPlanC implements Promotion {
@Override
public void promote() {
System.out.println("促销方案 C");
}
}

定义上下文对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author mofan 2021/2/27
*/
public class Context {
private Promotion promotion;

public Context(Promotion promotion) {
this.promotion = promotion;
}

public void userPromotionPlan() {
promotion.promote();
}
}

最后进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author mofan 2021/2/27
*/
public class PromotionTest {
@Test
public void testPromotion() {
// 使用促销方案 A
Context contextA = new Context(new PromotionPlanA());
contextA.userPromotionPlan();

// 使用促销方案 B
Context contextB = new Context(new PromotionPlanB());
contextB.userPromotionPlan();

// 使用促销方案 C
Context contextC = new Context(new PromotionPlanC());
contextC.userPromotionPlan();

Context contextD = new Context(() -> System.out.println("促销方案 D"));
contextD.userPromotionPlan();
}
}

运行上述测试代码后,控制台打印出:

促销方案 A
促销方案 B
促销方案 C
促销方案 D

3.4 模式的结构

策略模式是准备一组算法,并将这组算法封装到一系列的策略类里面,作为一个抽象策略类的子类。策略模式的重心不是如何实现算法,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性,现在我们来分析其基本结构和实现方法。

策略模式的主要角色如下:

1、抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。

2、具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。

3、环境(Context)类:持有一个策略类的引用,最终给客户端调用。

策略模式结构图:

策略模式的结构图

3.5 总结与反思

虽然上述的案例代码很简单,但是也具备了 策略模式 的雏形,其具体使用可见一斑。

在测试类的最后,我们又定义了一种促销方案,并且使用了 Lambda 表达式,Lambda 表达式是 Java 8 的新特性,它允许以参数的方式传递方法,感兴趣可以自行了解。

说到这里,遥想一下在 JDK 中哪些地方使用了 策略模式 呢?

给点提示:以接口的方式抽象出算法,然后根据需要具体实现算法。

很明显,Arrays.sort() 方法就使用了策略模式。准确的说,是 Arrays 中的这个 sort() 方法(该方法有很多重载):

1
2
3
4
5
6
7
8
9
10
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}

看,是不是很明显就能看出使用了 策略模式?首先有一个抽象策略类 Comparator,而环境类就是 Arrays,只不过 sort() 方法是静态的,直接使用 . 进行调用即可,至于具体的策略类,则是交给用户自己编写,以达到自定义排序方式的目的。

那怎么使用呢?

方式也有很多,这就涉及到 Java 基础了:使用实现类、静态嵌套类、局部内部类、匿名内部类,Lambda 表达式等等几种方式。

示例一下如何利用匿名内部类和 Lambda 进行使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testSort() {
Integer[] array = {9, 8, 7, 1, 2, 3};
Arrays.sort(array, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
System.out.println(Arrays.toString(array));

Arrays.sort(array, (o1, o2) -> o2 - o1);
System.out.println(Arrays.toString(array));
}

运行后,控制台打印出:

[1, 2, 3, 7, 8, 9]
[9, 8, 7, 3, 2, 1]

4. 经典鸭子案例

4.1 需求的提出

前文的例子比较简单,想看看比较复杂的案例,可以看看本案例。

我奸商的本性逐渐暴露,消费者们都意识到自己被骗了,生意日渐惨淡,最终,我破产了。

为了更好的活下去,我选择了养鸭子。

但我有远大的理想,我要垄断中国的鸭市场,无论你需要的是家鸭、野鸭,抑或是北京烤鸭,玩具鸭,只要是和鸭有关的,都可以从我这进货。

那么问题来了,这么多鸭,应该怎么描述它们呢?

4.2 初步解决

创业初期,规模不大,因此暂时只有三种鸭子:

野鸭:这种鸭子会飞、会叫、会游泳;

玩具鸭:这种鸭子会叫、会飞,不能游泳;

北京烤鸭:这种鸭子只能被吃,其他啥都不会。

对此,我们可以先写一个 Duck 抽象类,里面定义一些与鸭子行为相关的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author mofan 2021/2/27
*/
public abstract class Duck {

/**
* 展示鸭子信息
*/
public abstract void display();

public void quack() {
System.out.println("鸭子会叫 ~~");
}

public void swim() {
System.out.println("鸭子会下水 ~~");
}

public void fly() {
System.out.println("鸭子会上天 ~~");
}
}

具体的鸭子只需要继承这个类,然后针对自己所具备的特性进行方法的重写即可。比如:

1
2
3
4
5
6
7
8
9
/**
* @author mofan 2021/2/27
*/
public class WildDuck extends Duck {
@Override
public void display() {
System.out.println("这是一只野鸭!");
}
}

而玩具鸭不能游泳,因此重写 swim() 方法,抛出异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author mofan 2021/2/27
*/
public class ToyDuck extends Duck{
@Override
public void display() {
System.out.println("这是一只玩具鸭!");
}

@Override
public void swim() {
throw new UnsupportedOperationException();
}
}

那针对北京烤鸭来说就啥都不行了,得重写所有方法并抛出异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author mofan 2021/2/27
*/
public class PekingDuck extends Duck {
@Override
public void display() {
System.out.println("这是一只北京烤鸭!");
}

@Override
public void quack() {
throw new UnsupportedOperationException();
}

@Override
public void swim() {
throw new UnsupportedOperationException();
}

@Override
public void fly() {
throw new UnsupportedOperationException();
}
}

但是聪明的你应该已经发现了其中的问题:

1、首先所有鸭子都继承了 Duck 类,让所有鸭子都会飞、都会叫、都会游泳,这显然是不对的;

2、如果改变 Duck 超类,会影响其他的类;

3、针对第一个问题可以采用方法的重写来解决,但是如果遇到像北京烤鸭这种,还要对方法进行全部覆盖。

这个时候可以使用 策略模式 来解决。

4.3 使用策略模式

使用方式跟前面一样,抽象出具体的行为,然后利用接口来编码。

备注: 为了减少不必要的代码,在接下来的代码中我将使用 Lambda 表达式。

先编写各种接口,用于描述鸭子的行为:

1
2
3
4
5
6
7
8
9
/**
* @author mofan 2021/2/27
*/
public interface FlyBehavior {
/**
* 鸭子的飞行行为
*/
void fly();
}
1
2
3
4
5
6
7
8
9
/**
* @author mofan 2021/2/27
*/
public interface QuackBehavior {
/**
* 鸭子的叫
*/
void quack();
}
1
2
3
4
5
6
7
8
9
/**
* @author mofan 2021/2/27
*/
public interface SwimBehavior {
/**
* 鸭子的游泳行为
*/
void swim();
}

使用定义的接口,改造抽象类:

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
/**
* @author mofan 2021/2/27
*/
public abstract class Duck {
protected FlyBehavior flyBehavior;
protected SwimBehavior swimBehavior;
protected QuackBehavior quackBehavior;

/**
* 展示鸭子信息
*/
public abstract void display();

public void quack() {
if (quackBehavior != null) {
quackBehavior.quack();
}
}

public void swim() {
if (swimBehavior != null) {
swimBehavior.swim();
}
}

public void fly() {
if (flyBehavior != null) {
flyBehavior.fly();
}
}
}

具体的鸭子具体分析

野鸭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author mofan 2021/2/27
*/
public class WildDuck extends Duck {
public WildDuck() {
flyBehavior = () -> {
System.out.println("野鸭能飞");
};
swimBehavior = () -> {
System.out.println("野鸭能游泳");
};
quackBehavior = () -> {
System.out.println("野鸭能叫");
};
}

@Override
public void display() {
System.out.println("这是一只野鸭!");
}
}

玩具鸭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author mofan 2021/2/27
*/
public class ToyDuck extends Duck{

public ToyDuck() {
flyBehavior = () -> {
System.out.println("这鸭子能飞");
};
swimBehavior = () -> {
throw new UnsupportedOperationException();
};
quackBehavior = () -> {
System.out.println("这鸭子能叫");
};
}

@Override
public void display() {
System.out.println("这是一只玩具鸭!");
}
}

北京烤鸭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author mofan 2021/2/27
*/
public class PekingDuck extends Duck {

public PekingDuck() {
flyBehavior = () -> { throw new UnsupportedOperationException(); };
swimBehavior = () -> { throw new UnsupportedOperationException(); };
quackBehavior = () -> { throw new UnsupportedOperationException(); };
}

@Override
public void display() {
System.out.println("这是一只北京烤鸭!");
}
}

具体的测试

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
/**
* @author mofan 2021/2/27
*/
public class DuckTest {
@Test
public void testDuck() {
PekingDuck pekingDuck = new PekingDuck();
try {
pekingDuck.fly();
Assert.fail();
} catch (Exception e) {
Assert.assertTrue(e instanceof UnsupportedOperationException);
}

ToyDuck toyDuck = new ToyDuck();
try {
toyDuck.swim();
Assert.fail();
} catch (Exception e) {
Assert.assertTrue(e instanceof UnsupportedOperationException);
}

WildDuck wildDuck = new WildDuck();
wildDuck.fly();
wildDuck.swim();
wildDuck.quack();
}
}

运行测试方法后,测试通过,控制台打印出:

野鸭能飞
野鸭能游泳
野鸭能叫

5. 与 Spring 的整合

在工程开发中,基本都会使用到 Spring,策略模式与 Spring 整合后也能更方便使用。

还是以鸭子案例为基础,三个鸭子行为接口不变,额外提供 DuckType 枚举,描述鸭子的种类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author mofan
* @date 2022/10/6 15:06
*/
public enum DuckType {
/**
* 野鸭
*/
WILD,

/**
* 玩具鸭
*/
TOY,

/**
* 北京烤鸭
*/
PEKING
}

Duck 抽象类也需要改动一下,提供获取当前鸭子种类的抽象方法:

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
33
34
/**
* @author mofan
* @date 2022/10/6 15:02
*/
public abstract class Duck {
protected FlyBehavior flyBehavior;
protected QuackBehavior quackBehavior;
protected SwimBehavior swimBehavior;

/**
* 获取鸭子的类型
*
* @return 鸭子类型
*/
public abstract DuckType getDuckType();

public void fly() {
if (flyBehavior != null) {
flyBehavior.fly();
}
}

public void quack() {
if (quackBehavior != null) {
quackBehavior.quack();
}
}

public void swim() {
if (swimBehavior != null) {
swimBehavior.swim();
}
}
}

然后是在三种具体的鸭子类中重写 getDuckType() 方法,并且 将它们交给 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
/**
* @author mofan
* @date 2022/10/6 15:08
*/
@Component
public class WildDuck extends Duck {
@Override
public DuckType getDuckType() {
return DuckType.WILD;
}

public WildDuck() {
flyBehavior = () -> {
System.out.println("野鸭能飞");
};

swimBehavior = () -> {
System.out.println("野鸭能游泳");
};

quackBehavior = () -> {
System.out.println("野鸭能叫");
};
}
}
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
/**
* @author mofan
* @date 2022/10/6 15:11
*/
@Component
public class ToyDuck extends Duck {
@Override
public DuckType getDuckType() {
return DuckType.TOY;
}

public ToyDuck() {
flyBehavior = () -> {
System.out.println("玩具鸭能飞");
};

swimBehavior = () -> {
throw new UnsupportedOperationException();
};

quackBehavior = () -> {
System.out.println("玩具鸭能叫");
};
}
}
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
/**
* @author mofan
* @date 2022/10/6 15:12
*/
@Component
public class PekingDuck extends Duck {
@Override
public DuckType getDuckType() {
return DuckType.PEKING;
}

public PekingDuck() {
flyBehavior = () -> {
throw new UnsupportedOperationException();
};

swimBehavior = () -> {
throw new UnsupportedOperationException();
};

quackBehavior = () -> {
throw new UnsupportedOperationException();
};
}
}

然后编写 StrategyDuckService 类,该类也需要交由 Spring 管理并实现 ApplicationContextAware 接口,在 Spring 创建 Bean 时会自动 setApplicationContext() 方法,在这个方法里可以将交由 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
26
27
28
29
30
31
32
33
34
35
36
/**
* @author mofan
* @date 2022/10/6 15:14
*/
@Component
public class StrategyDuckService implements ApplicationContextAware {

private final Map<DuckType, Duck> duckStrategyMap = new ConcurrentHashMap<>();

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, Duck> duckBeanMap = applicationContext.getBeansOfType(Duck.class);
duckBeanMap.values().forEach(i -> duckStrategyMap.put(i.getDuckType(), i));
}

public void fly(DuckType type) {
Duck duck = duckStrategyMap.get(type);
if (duck != null) {
duck.fly();
}
}

public void quack(DuckType type) {
Duck duck = duckStrategyMap.get(type);
if (duck != null) {
duck.quack();
}
}

public void swim(DuckType type) {
Duck duck = duckStrategyMap.get(type);
if (duck != null) {
duck.swim();
}
}
}

到此,策略模式与 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
26
27
@Autowired
private StrategyDuckService duckService;

@Test
public void testStrategy() {
try {
duckService.fly(DuckType.PEKING);
Assert.fail();
} catch (Exception e) {
Assert.assertTrue(e instanceof UnsupportedOperationException);
}

try {
duckService.swim(DuckType.TOY);
Assert.fail();
} catch (Exception e) {
Assert.assertTrue(e instanceof UnsupportedOperationException);
}

try {
duckService.swim(DuckType.WILD);
duckService.fly(DuckType.WILD);
duckService.quack(DuckType.WILD);
} catch (Exception e) {
Assert.fail();
}
}

运行测试方法后,测试通过,控制台打印出:

野鸭能游泳
野鸭能飞
野鸭能叫

6. 与 Lambda 的结合

6.1 问题与改进

所谓策略模式,就是将多个算法或行为封装起来,使它们能够在运行时更改。再简单点,通过传不同的值干不同的事,那这不就是 if...else

确实是这么回事,只不过抽了个接口,搞了几个实现类,然后再进行逻辑分派罢了,在最后的逻辑分派中,和 if...else 没啥两样,但使用策略模式在复杂场景中会更好维护。

有时候难免会遇到想用而又不想用策略模式的情况,这听起来有些矛盾,其实是真实存在的。比如业务谈不上复杂,但是 if...else 的判断有很多,此时结合起来就显得有些复杂了。想用策略模式吧,又会徒增几个类,感觉不太划算,不用策略模式吧,多个 if...else 又很恶心。那这个时候该怎么办呢?

在这情况下如果硬要使用传统的策略模式,会发现 设计的接口中有且仅有一个抽象方法。

这不就是函数式接口吗?既然如此,使用 Lambda 表达式来完成是再合适不过了。

实现类增多的问题解决了,现在还有个问题:怎么通过传不同的值干不同的事呢?

如果把这里的“事”当成一个 Lambda 表达式,问题就可以换成:怎么通过传不同的值得到不同的 Lambda 表达式呢?

Lambda 表达式可以看成一个值,问题又换成:怎么通过传不同的值而得到不同的值呢?

这不就是 Map

离目标只有一步之遥了,现在最后的问题是:怎么初始化这个 Map,或者说怎么往这个 Map 里设置好初始值,后续使用时直接 get() 就完事了。

可以考虑把这个 Map 设置为类的常量,其初始化步骤在静态代码块中完成,静态代码块与静态变量之间的执行顺序取决于它们在代码中出现的顺序,因此要求 Map 的设置要在初始化步骤之前。

这样的做法并不很优雅,实际工程中一般会使用到 Spring,可以利用 Spring 的生命周期,使这个 Map 在 Spring 容器启动过程中被初始化,而这显然需要使用到注解 @PostConstruct

Spring 在完成 Bean 的依赖注入后,就会执行被此注解标记的方法,那么在这个方法里完成 Map 的初始化是再合适不过的了。

OK,来试试。😏

6.2 具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author mofan
* @date 2022/10/24 21:43
*/
@Component
public class LambdaStrategyComponent {

private static final Map<String, Function<String, String>> SERVICE_DISPATCHER = new HashMap<>();

public static final String CONSTANT = "执行业务: ";

@PostConstruct
public void initDispatcher() {
SERVICE_DISPATCHER.put("1", param -> CONSTANT + param);
SERVICE_DISPATCHER.put("2", param -> CONSTANT + param);
SERVICE_DISPATCHER.put("3", param -> CONSTANT + param);
}

public String execute(String type, String param) {
Function<String, String> function = SERVICE_DISPATCHER.get(type);
return function != null ? function.apply(param) : null;
}
}

好,实现完成,这非常简单:传入不同的值,返回不同的结果。

如果不这样处理,execute() 方法的实现会像这样:

1
2
3
4
5
6
7
8
9
10
11
public String execute(String type, String param) {
if ("1".equals(type)) {
return CONSTANT + param;
} else if ("2".equals(type)) {
return CONSTANT + param;
} else if ("3".equals(type)) {
return CONSTANT + param;
} else {
return null;
}
}

这一次又一次的 if 看着十分恶心,而且这还是在 Demo 的情况下,实际场景中需求更复杂,不仅有一次又一次的 if,还会有一次又一次的 if 嵌套。

简单测试下:

1
2
3
4
5
6
7
8
9
10
@Autowired
private LambdaStrategyComponent component;

@Test
public void testLambdaStrategy() {
Assert.assertEquals(LambdaStrategyComponent.CONSTANT + "1", component.execute("1", "1"));
Assert.assertEquals(LambdaStrategyComponent.CONSTANT + "2", component.execute("2", "2"));
Assert.assertEquals(LambdaStrategyComponent.CONSTANT + "3", component.execute("3", "3"));
Assert.assertNull(component.execute("4", "4"));
}

6.3 举一反三

问题似乎得到了解决,不妨举一反三下实际业务场景。

存在多个复杂的判断条件怎么办?

可以指定 Mapkey 的生成策略,在初始化 Map 时按照生成策略进行初始化,使用时先通过生成策略计算出 key,再从 Map 中获取具体实现。

具体实现很复杂,Map 的初始化方法很冗长

可以将不同的实现抽到一个独立的类中,然后注入到当前类里面,最后在初始化方法里调用下就可以了。

7. 策略枚举

还可以使用策略枚举对代码中的 if...else 进行优化。

我认为策略枚举的方式并没有使用 Lambda 来得优雅,枚举就应该是简单的、清爽的,嵌套过多的业务代码会让后期维护变得困难,但既然说都说到了,那还是介绍一下。

直接上代码:

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
33
34
35
36
37
/**
* @author mofan
* @date 2022/10/26 15:47
*/
public enum StrategyEnum {
/**
* 1
*/
ONE {
@Override
public String doSomething(String param) {
return StrategyConstants.CONSTANT + param;
}
},

/**
* 2
*/
TWO {
@Override
public String doSomething(String param) {
return StrategyConstants.CONSTANT + param;
}
},

/**
* 3
*/
THREE {
@Override
public String doSomething(String param) {
return StrategyConstants.CONSTANT + param;
}
};

public abstract String doSomething(String param);
}

测试一下:

1
2
3
4
5
6
@Test
public void testStrategyEnum() {
Assert.assertEquals(CONSTANT + "1", StrategyEnum.ONE.doSomething("1"));
Assert.assertEquals(CONSTANT + "2", StrategyEnum.TWO.doSomething("2"));
Assert.assertEquals(CONSTANT + "3", StrategyEnum.THREE.doSomething("3"));
}

再回过头来分析下这种写法:在每个枚举项中都重写了 doSomething() 方法,然后使用不同的枚举项就可以执行不同的实现,这与策略模式的思想几乎无异。

为什么在枚举里定义一个抽象方法后需要在每个枚举项中实现呢?

这和继承机制是类似的,可以把 StrategyEnum 当成一个父类,其中的每个枚举项是非抽象的子类,父类中存在的抽象方法必须在非抽象的子类中重写,因此就出现了这种写法。

如果有多个重复的条件判断应该怎么做呢?

可以将策略枚举作为另一个枚举的内部枚举,然后外部的多个枚举项指向同一个内部策略枚举项即可。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* @author mofan
* @date 2022/10/26 15:57
*/
public enum ComplexStrategyEnum {
/**
* 1
*/
FIRST("1", SimpleStrategyEnum.ONE),
/**
* 2
*/
SECOND("2", SimpleStrategyEnum.TWO),
/**
* 3
*/
THIRD("3", SimpleStrategyEnum.ONE);

private final String code;
private final SimpleStrategyEnum strategy;

ComplexStrategyEnum(String code, SimpleStrategyEnum strategy) {
this.code = code;
this.strategy = strategy;
}

public String toDo() {
return strategy.doSomething();
}

private enum SimpleStrategyEnum {
/**
* 1
*/
ONE {
@Override
String doSomething() {
return "ONE";
}
},

/**
* 2
*/
TWO {
@Override
String doSomething() {
return "TWO";
}
};

abstract String doSomething();
}
}
1
2
3
4
5
6
@Test
public void testComplexStrategyEnum() {
Assert.assertEquals("ONE", ComplexStrategyEnum.FIRST.toDo());
Assert.assertEquals("TWO", ComplexStrategyEnum.SECOND.toDo());
Assert.assertEquals("ONE", ComplexStrategyEnum.THIRD.toDo());
}