封面来源:碧蓝航线 蝶海梦花 活动CG

本文参考:

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();
}
}

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

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