序言——缺陷的业务代码
假如现在你需要做一个针对第三方接口的监控预警功能(配置的指标超出阈值进行短信邮件预警),需要计算应用集成的第三方接口的各种指标,比如:
- 接口费用
- 接口调用量
- 接口请求成功率
- 接口请求失败次数
- 接口剩余使用天数
- 接口响应时间超时次数
由于这些指标分别存储在 MySQL、Redis、ElasticSearch,因此计算的逻辑并不相同,所以你可能会写出下面的代码:
1 | // 指标枚举 |
1 | public getCalculateValue(...){ |
由于各个指标的计算方式并不相同,所以需要分别写代码进行计算,抽离出了各种方法。
从代码设计可以发现:
- 代码复杂性较高:switch (或 if-else) 的分支很多且业务代码很长,即使抽离为单独的方法,但一个类中还是存在了大量的代码
- 设计上不符合开闭原则(对修改关闭,对扩展开放):未来或许要新增、删除或修改指标。若在一个函数中来回修改无疑是件恐怖的事情,你不知道产品和客户什么想法,可能今天要你删个指标,明天又要去你加回来
为了降低代码复杂性,使设计解耦,让程序看起来简洁且能应对未来的变化,可以使用策略模式来优化原有代码,最终改造后代码可以变成这样:1
2
3
4
5
6
7// 优化后的代码
public getCalculateValue(...){
Integer indexType = index.getType();
IndexStatisticsStrategy routedStrategy = indexStatisticsStrategyContext.getRoutedStrategy(indexType);
Double calculateValue = routedStrategy.calculate(indexType);
return calculateValue;
}
改造后的代码看起来是不是非常简洁?
那么,策略模式是什么,又如何使用呢?
下面跟随本文来了解下吧!
初识——策略模式的基本定义
策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如每个人都要“交个人所得税”,但是“在中国交个人所得税”和“在美国交个人所得税”就有不同的算税方法。
许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法,因此策略模式:
- 定义了一族算法(不同的业务规则)
- 封装每个算法
- 这族的算法可互换代替(interchangeable)
实战运用
对序言中的业务示例而言,不同的指标计算方法对应着不同的计算规则,这些不同的指标计算规则若放到策略模式中,它们就代表一个个算法,在抽象维度上属于同一层次(一族算法),可以单独地新增、删除,互相替换。
那么,下面我们就通过策略模式来修改序言中的代码,来尝尝策略模式的味道!
改造的具体步骤大致如下:
- ① 定义抽象策略接口,通过接口运用多态可以灵活的替换不同的策略
- ② 定义具体策略实现类(算法),实现抽象策略接口,在内部封装具体的业务实现
- ③ 定义策略上下文,封装维护所有实现的策略(算法),可根据唯一标识路由到指定策略以执行,对客户端屏蔽具体的创建细节
- ④ 将代码中的 switch(或 if-else)替换为按类型从策略上下文获取指定策略以执行
① 定义抽象策略接口
每一个指标都有不同的名字,每一个指标的计算方式也不同,那么我们抽象这些维度,定义一个指标策略接口,让后续的实现类去重写实现:1
2
3
4
5
6
7
8
9public interface IndexStatisticsStrategy {
Integer indexSymbol();
/**
* 请随业务自定义实现
*/
Double calculate(Object o);
}
这里为什么不定义抽象父类,然后让子类去继承呢?父子类间不也可以使用多态吗?
确实如此,但接口通常比抽象类更有弹性,一般情况下若无特殊需要建议优先使用接口,设计原则中可是存在一条——多用组合,少用继承的原则呢!
具体而言,我们还是考虑具体的业务场景,这里我们真的需要父类吗?继承相同行为子类不重写?很明显并不需要。
② 定义具体策略实现类
第二步,我们将各种指标从原有设计中抽离出来,均实现策略接口并进行业务重写:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class InvokeQuantityIndexStrategy implements IndexStatisticsStrategy {
public Integer indexSymbol() {
return IndexStatisticsTypeEnum.INVOKE_QUANTITY.ordinal();
}
/**
* 请随业务自定义实现
*/
public Double calculate(Object o) {
// todo
return Double.valueOf(count);
}
}
1 | public class RequestFailedQuantityIndexStrategy implements IndexStatisticsStrategy { |
③ 定义策略上下文
第三步,策略肯定不应直接获取,将它们统一维护管理起来,让调用者获取策略时通过策略上下文间接获取:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* 策略上下文:
* - 维护所有策略
* - 可根据唯一标识路由到指定策略以执行
*
* @author Chris Wong
* @since 2021/10/18
*/
public class IndexStatisticsStrategyContext {
private static final Map<Integer, IndexStatisticsStrategy> indexStatisticsStrategyMap = new HashMap<>();
static {
indexStatisticsStrategyMap.put(IndexStatisticsTypeEnum.INVOKE_QUANTITY.ordinal(), new InvokeQuantityIndexStrategy);
indexStatisticsStrategyMap.put(IndexStatisticsTypeEnum.FAILED_QUANTITY.ordinal(), new RequestFailedQuantityIndexStrategy);
}
public IndexStatisticsStrategy getRoutedStrategy(int symbol) {
return indexStatisticsStrategyMap.get(symbol);
}
}
④ 替换原有多条件分支
最后就是在代码中具体使用了:1
2
3
4
5
6
7// 优化后的代码
public getCalculateValue(...){
Integer indexType = index.getType();
IndexStatisticsStrategy routedStrategy = indexStatisticsStrategyContext.getRoutedStrategy(indexType);
Double calculateValue = routedStrategy.calculate(indexType);
return calculateValue;
}
扩展优化——开闭原则的策略上下文
在设计模式中,存在一个设计原则——开闭原则:对修改关闭,对扩展开放(在尽可能不修改原有代码的情况下,增加新功能的代码)。
在前面的代码中,虽然具体的策略类是从策略上下文中获取的,也确实取消了switch( if-else)设计,通过策略上下文可以获取到策略类,最终能够执行具体的策略实现业务。
但是,若现在要新增一个计算策略的实现类,就需要去改动策略上下文中的代码(往 Map 中 put 新增的策略实现类),此种设计,并不符合开闭原则。
那么,如何让代码遵循开闭原则呢?
答案非常简单,我们借助 Spring 进行动态管理所有策略类,将策略上下文代码适当修改即可:
1 | /** |
修改后,即使代码中新增了策略,也无需修改原有任何代码,Spring 启动时将自动加载新的策略进入策略上下文。
注意哦:要想借助 Spring 管理策略,需要让 Spring 对对象进行管理,因此除了在策略上下文增加@Component注解,所有的策略实现类也需要增加@Component注解。
角色组成
对策略模式而言,其基本结构类图如下:

由上图可知,策略模式主要包含三个角色:
- 上下文角色(StrategyContext):统一维护所有策略,封装可能存在的变化;间接操作策略, 屏蔽高层模块(客户端)对策略、算法的直接访问
- 抽象策略角色(IStrategy):规定策略或算法的行为抽象纬度
- 具体策略角色(ConcreteStrategy):具体的策略或算法实现
优与劣
优点
- 可以在运行时动态切换对象内的算法
- 可以将算法的实现和使用算法的代码隔离开来以提高算法的保密性和安全性
- 可以使用组合来代替继承(当然这不代表策略模式不能使用继承方式实现)
- 符合开闭原则,因此无需对上下文进行修改就能够引入新的策略
缺点
- 客户端必须知晓策略间的不同——因为它需要选择合适的策略
- 若算法过于简单或者极少发生改变, 那么没有任何理由引入新的类和接口,此时使用该模式只会让程序过于复杂
适用场景
当存在以下情况时可使用 Strategy 模式:
- 需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时,可以使用策略模式。
- 算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将相关的条件分支移入它们各自的 Strategy 类中(仅建议在条件分支代码复杂时使用)
结语
本文通过对实际业务场景代码的剖析,发现了其实现方式的不足之处,借助实战以策略模式的思想重构优化了代码。在此过程中,我们由浅及深地认识了策略模式,知道了其定义,发现了其组成角色,了解了其优缺点,并最终学会了如何具体地运用到实际的业务中。
本文思维导图如下:

参考
- 谭勇德. 设计模式就该这样学 [M]. 电子工业出版社,2020
- Eric Freeman. HeadFirst 设计模式 [M]. 中国电力出版社,2007
文章信息
| 时间 | 说明 |
|---|---|
| 2020-12-03 | 大改 |
| 2021-10-19 | 重构,剥离初始文章 |
| 2022-07-18 | 重排版,增加图片说明 |