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

1. 定义及特点

策略模式定义了一系列算法,并将它们封装起来,使它们可以相互替换。这些算法被封装在独立的策略类中,客户端通过策略接口来调用算法,而不需要知道具体的算法实现。这样,算法的变化就不会影响到使用这些算法的客户,从而实现了算法与客户端的解耦。

优点

  • 算法的自由切换:策略模式使得算法可以独立于使用它们的客户端而变化,从而可以在运行时动态地切换算法。
  • 避免使用多重条件语句:通过将算法封装在独立的策略类中,可以避免在客户端代码中使用大量的 if-elseswitch 语句来选择算法,从而提高代码的可读性和可维护性。
  • 提高扩展性:当需要添加新的算法时,只需要新增一个策略类实现策略接口即可,无需修改原有的代码,符合开闭原则(对扩展开放,对修改关闭)。
  • 算法的可重用性:策略模式中的算法被封装在独立的类中,可以被多个客户端重复使用,提高了算法的可重用性。
  • 算法的保密性和安全性:客户端只需要知道策略接口,而不需要知道具体的算法实现,这在一定程度上提高了算法的保密性和安全性。

缺点

  • 客户端必须了解所有策略:客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这意味着客户端需要理解这些算法的区别,以便在适当的时候选择正确的算法。这可能会增加客户端的复杂性。
  • 增加对象的数目:策略模式将每个算法都封装在独立的类中,如果算法很多,那么就会产生大量的策略类。这可能会增加系统中对象的数量,从而增加系统的复杂性和维护成本。
  • 策略类的设计难度:设计合理的策略类接口和具体的策略类需要一定的技巧和经验。如果设计不当,可能会导致策略类之间的耦合度过高,从而影响系统的灵活性和可扩展性。

2. 适用场景

策略模式适用于以下场景:

  • 存在多种算法或行为,且需要根据不同条件或需求进行切换。
  • 算法或行为频繁变化,且这些变化可能影响到多个客户端。
  • 需要将算法或行为封装起来,以提高代码的模块化和可重用性。
  • 系统需要支持可扩展性,能够在不修改现有代码的情况下添加新的算法或行为。
  • 算法或行为具有相似的接口,但实现不同。
  • 数据处理存在多种方式,且这些方式需要根据数据的不同特性或需求进行选择。
  • 业务逻辑复杂且规则频繁变更的系统中,用于封装不同的业务规则。

3. 简单应用

3.1 场景假设

假设你经营着一家餐厅,为了吸引顾客,你决定在一段时间内对菜单上的所有菜品进行折扣促销。最初,你决定所有菜品都享受8折优惠。

顾客来点餐时,服务员会根据菜单上的原价,简单地乘以0.8来计算折扣后的价格。比如,一道原价100元的菜品,现在只需支付80元。

然而,促销活动进行了一段时间后,你发现8折的优惠力度过大,影响了餐厅的利润。于是,你决定紧急调整折扣策略,改为9折优惠。这时,服务员需要记住新的折扣率,并在计算价格时改用0.9来乘以原价。

但好景不长,随着市场竞争的加剧和你对利润的追求,你可能需要频繁地调整折扣策略。比如,为了吸引更多的家庭顾客,你可能推出“满300元减50元”的优惠活动。这时,服务员就需要根据消费金额的不同,选择不同的折扣计算方式,这无疑增加了出错的概率。

更重要的是,如果你的餐厅有多个分店,每个分店都可能有自己的促销活动,而每个活动都需要服务员记住不同的折扣计算方式。这不仅增加了培训成本,还可能导致服务不一致,影响顾客体验。

这显然不是一种很好的解决方式。

3.2 解决方案

那么应该怎么解决这个问题呢?我们可以这样做:

想象我们拥有一个类似“锦囊”的机制,每个“锦囊”中都封装着一种计算商品价格的算法。当遇到不同的价格计算需求时,我们只需选择并打开相应的“锦囊”即可。

为了实现这种机制,我们可以定义一个接口(或抽象类),该接口声明了所有价格计算算法所共有的方法。然后,我们为每一种具体的价格计算方式实现这个接口,每个实现都可以看作是一个“锦囊”。

为了在应用中使用这些“锦囊”,我们还需要一个上下文类(Context),它持有一个接口类型的成员变量,这个成员变量在运行时可以指向任何一个实现了价格计算接口的对象。上下文类提供了一个接口来设置这个成员变量的值(即选择“锦囊”),并允许客户通过该接口来执行价格计算。

通过这种方式,我们实现了算法的选择与使用的解耦。高层模块(即需要计算价格的模块)不需要直接知道具体使用了哪种价格计算算法,它只需要通过上下文类来调用算法即可。这种设计不仅提高了系统的灵活性,还使得算法的变化不会影响到其他部分的代码,从而降低了系统的维护成本。

3.3 代码实现

先准备一个折扣策略接口,定义了所有折扣策略必须实现的计算方法:

1
2
3
4
5
6
7
8
9
/**
* 折扣策略接口
*/
public interface DiscountStrategy {
/**
* 计算折扣
*/
void calculateDiscount();
}

然后编写折扣方法的具体实现类,编写不同的折扣方案:

8折折扣策略实现:

1
2
3
4
5
6
public class EightyPercentOffStrategy implements DiscountStrategy {
@Override
public void calculateDiscount() {
System.out.println("8折策略");
}
}

9折折扣策略实现:

1
2
3
4
5
6
public class NinetyPercentOffStrategy implements DiscountStrategy {
@Override
public void calculateDiscount() {
System.out.println("9折策略");
}
}

定义上下文类,用于管理折扣策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DiscountContext {

// 当前使用的折扣策略
private final DiscountStrategy strategy;

/**
* 构造方法,初始化折扣策略
*
* @param strategy 初始的折扣策略
*/
public DiscountContext(DiscountStrategy strategy) {
this.strategy = strategy;
}

/**
* 应用折扣策略计算折扣后的价格
*/
public void applyDiscount() {
strategy.calculateDiscount();
}

}

测试:

1
2
3
4
5
6
7
8
9
10
@Test
void testDiscount() {
// 8折策略
DiscountContext contextA = new DiscountContext(new EightyPercentOffStrategy());
contextA.applyDiscount();

// 9折策略
DiscountContext contextB = new DiscountContext(new NinetyPercentOffStrategy());
contextB.applyDiscount();
}

测试结果:

1
2
8折策略
9折策略

3.4 模式的结构

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

策略模式通常包括三个角色:

  • 策略接口(Strategy):定义所有支持的算法的公共接口。
  • 具体策略(ConcreteStrategy):封装了具体的算法或行为,并实现了策略接口。
  • 上下文(Context):接受客户的请求,随后把请求委托给某一个策略类。

策略模式结构图:

策略模式的结构图

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
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
public class WildDuck extends Duck {
@Override
public void display() {
System.out.println("这是一只野鸭!");
}
}

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

1
2
3
4
5
6
7
8
9
10
11
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
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 策略模式解决

与前面简单应用时一样,先抽象出具体的行为,然后利用接口来编码。

先编写鸭子不同行为的接口:

1
2
3
4
5
6
public interface FlyBehavior {
/**
* 鸭子飞
*/
void fly();
}
1
2
3
4
5
6
public interface QuackBehavior {
/**
* 鸭子叫
*/
void quack();
}
1
2
3
4
5
6
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
public abstract class Duck {
public FlyBehavior flyBehavior;
public SwimBehavior swimBehavior;
public 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
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
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
17
18
19
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
@Test
public void testDuck() {
PekingDuck pekingDuck = new PekingDuck();
try {
pekingDuck.fly();
Assertions.fail();
} catch (Exception e) {
Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
}

ToyDuck toyDuck = new ToyDuck();
try {
toyDuck.swim();
Assertions.fail();
} catch (Exception e) {
Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
}

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

测试结果:

1
2
3
鸭子会上天 ~~
鸭子会下水 ~~
鸭子会叫 ~~

5. JDK源码解析

在 JDK 中,Arrays类中有一个 sort() 方法就是使用了 策略模式,如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Arrays{
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);
}
}
}

Arrays 就是一个环境角色类,而抽象策略类为 Comparator ,这个 sort 方法可以传一个新策略让 Arrays 根据这个策略来进行排序,至于这个策略是什么,那就得让使用者自由定义了。

比如我想要将一个数组以升序的方式进行排序:

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

测试结果:

1
[1, 2, 2, 3, 4, 5, 12]

6. 在 SpringBoot 应用

在日常开发中通常会用到 SpringBoot ,那在 SpringBoot 中该怎样结合策略模式呢?

还是以上面鸭子案例作为基础,改写代码完成应用吧。

提供一个 DuckType 枚举,用于描述鸭子的种类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum DuckType {
/**
* 野鸭
*/
WILD_DUCK,

/**
* 玩具鸭
*/
TOY_DUCK,

/**
* 北京烤鸭
*/
PEKING_DUCK
}

Duck 抽象类的 display() 方法修改为获取当前鸭子种类的抽象方法,其他代码保持不变:

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
public abstract class Duck {
public FlyBehavior flyBehavior;
public SwimBehavior swimBehavior;
public QuackBehavior quackBehavior;

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

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

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

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

然后是在三种具体的鸭子类中重写 getDuckType() 方法,并且 将它们交给 Spring 管理, 其余部分基本不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class WildDuck extends Duck {

public WildDuck() {
flyBehavior = () -> {
System.out.println("野鸭能飞 ~~");
};
swimBehavior = () -> {
System.out.println("野鸭能游泳 ~~");
};
quackBehavior = () -> {
System.out.println("野鸭能叫 ~~");
};
}

@Override
public DuckType getDuckType() {
return DuckType.WILD_DUCK;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class ToyDuck extends Duck {

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

@Override
public DuckType getDuckType() {
return DuckType.TOY_DUCK;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class PekingDuck extends Duck {

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

@Override
public DuckType getDuckType() {
return DuckType.PEKING_DUCK;
}
}

然后创建 StrategyDuckContext 类,让它被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
@Component
public class StrategyDuckContext implements ApplicationContextAware {

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

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

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

测试:

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 StrategyDuckContext duckContext;

@Test
public void testStrategy() {
try {
duckContext.fly(DuckType.PEKING_DUCK);
Assertions.fail();
} catch (Exception e) {
Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
}

try {
duckContext.swim(DuckType.TOY_DUCK);
Assertions.fail();
} catch (Exception e) {
Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
}

try {
duckContext.swim(DuckType.WILD_DUCK);
duckContext.fly(DuckType.WILD_DUCK);
duckContext.quack(DuckType.WILD_DUCK);
} catch (Exception e) {
Assertions.fail();
}
}

测试结果:

1
2
3
野鸭能游泳 ~~
野鸭能飞 ~~
野鸭能叫 ~~