策略模式

行为型模式

问题

在一些场景下,请求需要委派给不同的对象来执行,对象所提供的方法输入和输出都是相同的,但是具体实现不同。在这种情况下,为了避免代码每次都被修改,需要使用“策略模式”。

策略模式的核心思想是对算法进行封装,委派给不同对象来管理。

策略模式(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 的自动注入。

与工厂模式的区别

策略模式和工厂模式有一定相似之处,在于它们的模式结构,因此有时候会让人混淆不清。

实际上,这两者之间存在较多差异:

  • 工厂模式是创建型模式,作用是创建对象,它关注对象如何创建,主要解决的是资源的统一分发,将对象的创建完全独立出来,让对象的创建和具体的使用客户无关;
  • 策略模式是行为型模式,作用是让一个对象在许多行为中选择一种行为,它关注行为如何封装,通过定义策略族来实现策略的灵活切换与扩展,并让策略的变化独立于使用策略的客户。