策略模式
行为型模式
问题
在一些场景下,请求需要委派给不同的对象来执行,对象所提供的方法输入和输出都是相同的,但是具体实现不同。在这种情况下,为了避免代码每次都被修改,需要使用“策略模式”。
策略模式的核心思想是对算法进行封装,委派给不同对象来管理。
策略模式(Strategy Pattern)对某种行为提供了一组实现策略,并把这一组策略分别封装在不同的实现类中。在执行行为时,根据上下文场景选择不同的具体实现,从而使得策略的变化独立于使用它的客户而变化。
组成
策略模式有3个参与者:
- 行为的抽象:可以看做是一个接口对外提供的一系列方法;
- 行为的一组实现策略:接口的一组实现类;
- 抽象行为的一个持有者:持有接口引用的对象,并且包含上下文信息,能够选择不同的实现类;
三者之间的关系如图1所示:
图1. 策略模式的组成结构
图1中各个各个组成介绍:
- Strategy:行为接口,定义了对外提供的方法;
- ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC:Strategy接口的具体实现类;
- Context:持有 Strategy 引用,并根据上下文条件选择不同的实现类来执行方法;
Context 根据客户请求的条件,选择出一个具体的 Strategy 实现类;当具体的 Strategy 被调用时,Context 把参数传入方法中。
对于客户而言,它仅需要与 Context 交互,并不知道具体执行任务的 Strategy 是哪一个,隔离了客户与 Strategy。这样,在后续迭代过程中修改代码时,仅需要修改 Strategy 的某个实现类,对“客户、Context” 都是无影响的。
应用场景
当存在以下情况时,推荐使用 策略模式:
- 存在许多相关的类,它们对外提供同一个方法,方法的执行逻辑功能相似,区别在于方法中的执行对象不同,那么可以使用一个 Context 从这些类中进行动态选择;
- 一个类定义了许多行为,这些行为以用多个条件语句的形式出现,那么可以把相关的条件分支移入各自的 Strategy 实现类中,用 Context 的选择代替条件语句的选择;
- 需要使用一个算法的不同变体,提供对接口/父类方法的多态实现。
变和不变
从“变化、不变”的角度来看“策略模式”:
不变的是“业务场景”,比如客户端调用 context,执行一个行为;
变化的是“参数和具体的行为”:客户端传给 context 的参数不同,需要执行不同的行为。
所以,策略模式是把业务场景中不变的“context、行为”抽象出来,把变化的具体行为实现与不变的部分隔离开来,保证了应用程序的可维护、可扩展。
示例
下面用一个例子来演示策略模式的常见应用场景。
业务的 Service 层需要根据 Controller 传入的条件,来执行相似的业务逻辑,演示代码如下:
我们可以在 Service 层中维护一个 StrategyFactory,StrategyFactory 根据条件选择不同的 Strategy 实现,代码如下:
public class Service {
StrategyFactory strategyFactory;
public void doMethod(int condition) {
Strategy strategy = strategyFactory.getStrategyByCondition(condition);
strategy.method();
}
}
public class StrategyFactory {
private Strategy strategy;
public Strategy getStrategyByCondition(int condition) {
// 根据条件返回不同的 strategy
}
}
public interface Strategy {
void method();
}
public class StrategyA implements Strategy {
@Override
public void method() {
// 逻辑A
}
}
public class StrategyB implements Strategy {
@Override
public void method() {
// 逻辑B
}
}
public class StrategyC implements Strategy {
@Override
public void method() {
// 逻辑C
}
}
public class StrategyD implements Strategy {
@Override
public void method() {
// 逻辑D
}
}
初始版本的 Service 类使用 if-else 做条件判断,虽然写起来很方便,但是如果条件增多,代码会变得越来越臃肿,而且违反了两个设计原则:
- 单一职责原则:一个类应该只有一个发生变化的原因,每次增删条件、修改条件的执行逻辑,Service 类都会被修改;
- 开闭原则:对扩展开放,对修改关闭,如果要增删新的逻辑,每次都会修改 Service 类;
使用策略模式之后,Service 类的代码更加简洁:
- 逻辑的实现细节被隐藏到具体的 Strategy 实现类中;
- 只有当 Service 本身的逻辑变化时,才会更改 Service,其他情况下不会修改 Service 类;
- 如果修改实现策略,可以修改 Strategy 的实现类,不需要修改 Service 类;
优点和缺点
从上面的演示示例可以看出策略模式的优点和缺点。
优点:
1、策略方法的抽象:Strategy 接口对外提供方法的抽象,具体的逻辑由它的实现子类提供;
2、使用组合替代继承:我们可以使用 Context 的多个子类,来实现多个逻辑的实现,但这样会把行为方法硬编码到 Context 中,使得 Context 难以理解、难以维护和难以扩展,使用策略模式把 Strategy 组合到 Context 中,可以使方法的实现独立于 Context ,使得它易于切换、理解、扩展;
3、消除了条件语句:使用具体的实现类封装条件逻辑,使用动态选择来代替 if-else/switch-case 的条件选择。
缺点:
1、客户和开发需要了解所有的策略,清楚它们的不同:Context 根据场景上下文来决定使用哪个 Strategy,场景上下文由客户传入;
- 这样具体策略难免暴露出去,并且要由上层模块初始化,这与迪米特法则相悖(最少知识原则),而上层模块和底层模之间的解耦,可以让工厂模式来完成。
2、增加了类的数量:每个实现类都封装为了一个策略类,类文件的个数会随着策略的增加而增加,并且每个 ConcreteStrategy 都要实现 Strategy 的所有方法,有些 ConcreteStrategy 并不会用到 Strategy 的所有方法;
3、Strategy 和 Context 之间的通信开销:Context 不能直接调用 Strategy 的具体实现对象,需要一个 StrategyFactory 类来维护 Strategy;
4、只适合扁平的代码结构:策略模式中各个策略的实现是平等的关系,而不是层级关系。
示例代码
策略模式关键在于两步:
- 如何把 Strategy 的具体实现“注入”到 Context 中;
- 如何根据条件选择出具体的 Strategy 实现类;
下面介绍两种条件下的示例。
示例1:非Spring框架
JDK 中 ThreadPoolExecutor 和它的拒绝策略 RejectedExecutionHandler 之间的关系,就类似于策略模式中 Context 与Strategy 之间的关系:ThreadPoolExecutor 是 Context,RejectedExecutionHandler 是 Strategy,两者之间的关系如图2所示:
图2. ThreadPoolExecutor 和 RejectedExecutionHandler
在非Spring框架中,要创建 ThreadPoolExecutor 对象的时候传入具体的实现策略:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
/*...省略 ...*/
new ThreadPoolExecutor.AbortPolicy());
ThreadPoolExecutor pool = new ThreadPoolExecutor(
/*...省略 ...*/
new ThreadPoolExecutor.DiscardOldestPolicy());
在非Spring框架下,我们需要在创建 Context 时,手动注入具体的策略对象。
示例2:Spring框架
Spring 会管理所有的 Bean,利用这一点,可以把所有 Strategy 的实现都注册到 Spring 容器中,然后 Context 从容器中取出 Bean,实现自动注入。如图3所示:
图3. Spring框架下的策略模式
Context 在注入具体策略时可以从 Spring 容器中取出,Context 和 Strategy 之间实现了一步的解耦。
@Component
public class FormSubmitHandlerFactory implements InitializingBean, ApplicationContextAware {
private static final
Map<String, FormSubmitHandler<Serializable>> FORM_SUBMIT_HANDLER_MAP = new HashMap<>(8);
private ApplicationContext appContext;
/
* 根据提交类型获取对应的处理器
*
* @param submitType 提交类型
* @return 提交类型对应的处理器
*/
public FormSubmitHandler<Serializable> getHandler(String submitType) {
return FORM_SUBMIT_HANDLER_MAP.get(submitType);
}
@Override
public void afterPropertiesSet() {
// 将 Spring 容器中所有的 FormSubmitHandler 注册到 FORM_SUBMIT_HANDLER_MAP
appContext.getBeansOfType(FormSubmitHandler.class)
.values()
.forEach(handler -> FORM_SUBMIT_HANDLER_MAP.put(handler.getSubmitType(), handler));
}
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
appContext = applicationContext;
}
}
在该示例代码中,StrategyFactory 利用了 Spring 框架自身的接口(InitializingBean 和 ApplicationContextAware),在 afterPropertiesSet 方法中取出容器中具体的 Strategy Bean 保存进 Map 中;当 Context 调用 Strategy 时,方法 getHandler() 从 Map 中取出 Bean。
这样做,在 Spring 容器启动时就实现了 Strategy Bean 的自动注入和维护,省去了“非Spring框架”下每次创建具体策略的步骤,实现了多策略的自动注入。
示例3:多策略的自动注入
我们在实际项目开发中,就仿照“示例2”的代码实现策略模式,比如我们要判断一个资源是否存在,资源的种类很多,我们可以创建一个“资源策略接口-Strategy ”,每种资源有对应“资源策略接口”的一个具体实现类,同时创建一个“资源策略工厂-StrategyFactory”,像“参考阅读2”一样,实现 Spring 的 InitializingBean 和 ApplicationContextAware 接口。
但是,随后发现了一个问题:在项目的其他业务场景下使用策略模式时,依然要创建一个“xxx策略工厂-xxxStrategyFactory”,依然要实现 Spring 的个接口,导致了重复代码的存在。
为了减少这种重复代码,我们在项目中,通过使用“泛型接口、注解、Spring的配置类”等方法法,实现了多策略工厂的自动注入。
配置类1
StrategyFactory 的主要方法有:
- `setApplicationContext`:设置 ApplicationContext 的引用;
- `afterPropertiesSet`:把 Strategy Bean 保存进 map 中;
- `getHandler`:根据 context 传入的 type 从 map 中取出 Strategy Bean;
其实,StrategyFactory 真正的功能只有两个:
- 把 Strategy Bean 保存进 map 中;
- 根据 context 传入的 type 从 map 中取出 Strategy Bean;
至于 ApplicationContext 的引用,可以在一个配置类中设置。
因此,我们创建一个配置类 StrategyConfig,实现 InitializingBean, ApplicationContextAware 接口,代码如下:
@Configuration
public class StrategyConfig implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
// 配置 StrategyFactory 和 Strategy Bean
}
}
在该配置类中,`setApplicationContext` 方法的作用就是设置 ApplicationContext 的引用;`afterPropertiesSet` 方法是用来配置 StrategyFactory 和 Strategy Bean,在后面 “配置类2” 部分有介绍。
泛型接口
一个 StrategyFactory 管理一个 Strategy 接口及其实现类,如果我们在多个业务场景下使用策略模式,就需要创建多个 StrategyFactory 类和多个 Strategy 接口。每个 Strategy 实现类在 StrategyFactory 的 map 中都有一个唯一的 key 值,比如每个 Strategy 实现类会返回一个 type 值,这个 type 值可以是字符串、数字等等。
我们把上述两个行为抽象出来,创建泛型 Strategy 接口和泛型 StrategyFactory 接口,代码如下:
/
* 泛型 Strategy 接口
* @author shimengjie
* @date 2021/11/5 14:39
/
public interface Strategy<K> {
/
* 返回类型
*
* @return K
*/
K getType();
}
/
* 泛型 StrategyFactory 接口
*
* @author shimengjie
* @date 2021/11/5 14:41
/
public interface StrategyFactory<K> {
/
* 添加 Strategy 实例
*
* @param k Strategy 的类型
* @param v Strategy 实例
*/
void addStrategy(K k, Strategy<K> v);
/
* 根据 key 值取出对应的 Strategy
*
* @param key Strategy 的类型
* @return Strategy 实例
*/
Strategy<K> getByType(K key);
}
项目中具体业务的 Strategy 接口、StrategyFactory 类都分别继承自这两个泛型接口:
- 泛型 <K> 就是每个 Strategy 返回的 type 类型;
- 因为其他的的 Strategy 接口都继承自 Strategy<K>,所以 StrategyFactory<K> 中`addStrategy` 方法参数、`getByType` 方法返回值,都是 Strategy<K>。
项目中具体业务的 Strategy 接口、StrategyFactory 类与这两个泛型接口的关系如图4(a)、图4(b) 所示:
图4(a). Strategy 接口与泛型 Strategy 接口的关系
图4(b). StrategyFactory 类与泛型 StrategyFactory 接口的关系
这样,我们就有了 Strategy 接口和 StrategyFactory 接口的统一抽象,但是,在配置类 StrategyConfig 的 afterPropertiesSet 方法中,我们该怎么知道每个 StrategyFactory 类管理的是哪个 Strategy 接口呢?
我们给 StrategyFactory 类添加注解,来标识它管理的 Strategy 接口。
注解
我们定义注解 `RegistryStrategyFactory`,它只有一个必填参数 strategy,值是泛型接口 Strategy<K> 的子类,代码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RegistryStrategyFactory {
/
* 指定策略实现类
*/
Class<? extends Strategy> strategy();
}
比如,我们创建 WorkStrategyFactory 类来管理 WorkStrategy 接口,只需要在 StrategyFactory 类上添加该注解即可,代码如下:
public interface WorkStrategy extends Strategy<String> {
/
* 判断作品是否存在
*
* @param id 作品ID
* @return true/false
*/
boolean isExisted(Long id);
}
@Component
@RegistryStrategyFactory(strategy = WorkStrategy.class)
public class WorkStrategyFactory implements StrategyFactory<String> {
/
* 保存 WorkStrategy 实例
*/
private Map<String, WorkStrategy> map;
public WorkStrategyFactory() {
this.map = new HashMap<>();
}
@Override
public void addStrategy(String k, Strategy<String> v) {
map.put(k, (WorkStrategy) v);
}
@Override
public WorkStrategy getByType(String key) {
return map.get(key);
}
}
配置类2
有了泛型接口、注解,我们可以完成配置类 StrategyConfig 的 `afterPropertiesSet` 方法。很显然,该方法主要步骤如下:
- 找出所有的 StrategyFactory Bean;
- 从每个 StrategyFactory Bean 的注解上找到它管理的 Strategy 接口;
- StrategyFactory Bean 把 Strategy Bean 添加到 map 中。
@Override
public void afterPropertiesSet() throws Exception {
// 遍历注册的 StrategyFactory
for (StrategyFactory factory : applicationContext.getBeansOfType(StrategyFactory.class).values()) {
RegistryStrategyFactory annotation = AnnotationUtils.findAnnotation(factory.getClass(), RegistryStrategyFactory.class);
if (annotation != null) {
// 取出注解中指定的 策略 Bean
Class<? extends Strategy> strategyClazz = annotation.strategy();
Map<String, ? extends Strategy> map = applicationContext.getBeansOfType(strategyClazz);
// 添加进 map 中
for (Strategy value : map.values()) {
factory.addStrategy(value.getType(), value);
}
}
}
}
小结
经过上述改造,我们每次创建 StrategyFactory 类时,不再需要实现 InitializingBean, ApplicationContextAware 接口,只需要实现泛型接口 StrategyFactory<K> 并添加注解,就可以实现 StrategyFactory 和 Strategy 的自动注入。
与工厂模式的区别
策略模式和工厂模式有一定相似之处,在于它们的模式结构,因此有时候会让人混淆不清。
实际上,这两者之间存在较多差异:
- 工厂模式是创建型模式,作用是创建对象,它关注对象如何创建,主要解决的是资源的统一分发,将对象的创建完全独立出来,让对象的创建和具体的使用客户无关;
- 策略模式是行为型模式,作用是让一个对象在许多行为中选择一种行为,它关注行为如何封装,通过定义策略族来实现策略的灵活切换与扩展,并让策略的变化独立于使用策略的客户。