设计模式-策略模式

设计模式-策略模式介绍了策略模式(Strategy Pattern)的基本概念和实践,然后对项目中策略模式的使用进行了优化。

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

行为型模式

策略模式(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 的选择代替条件语句的选择;
  • 需要使用一个算法的不同变体,提供对接口/父类方法的多态实现。

下面用一个例子来演示策略模式的常见应用场景:

业务的 Service 层需要根据 Controller 传入的条件,来执行相似的业务逻辑,演示代码如下:

public class Service {

    public void doMethod(int condition) {
       if (conditionA) {
    		   // 逻辑A
       } else if (conditionB) {
           // 逻辑B
       } else if (conditionC) {
           // 逻辑C
       } else {
           // 逻辑D
       }
    }
}

我们可以在 Service 层中维护一个 StrategyFactory(相当于 Context),StrategyFactory 根据条件选择不同的 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
    }
}

public class StrategyFactory   {
    
    private Strategy strategy;
   
    public Strategy getStrategyByCondition(int condition) {
        // 根据条件返回不同的 strategy
    }
}


public class Service {

    StrategyFactory strategyFactory;

    public void doMethod(int condition) {
        Strategy strategy = strategyFactory.getStrategyByCondition(condition);
        strategy.method();
    }
}

初始版本的 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 之间实现了一步的解耦。

下面的示例代码来自于参考阅读[2],演示了 StrategyFactory 在 Spring 框架下的实现:

@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框架”下每次创建具体策略的步骤,实现了

项目实践-多策略的自动注入

我们在实际项目开发中,就仿照“参考阅读2”的代码实现策略模式,比如我们要判断一个资源是否存在,资源的种类很多,我们可以创建一个“资源策略接口-Strategy ”,每种资源有对应“资源策略接口”的一个具体实现类,同时创建一个“资源策略工厂-StrategyFactory”,像“参考阅读2”一样,实现 Spring 的 InitializingBean 和 ApplicationContextAware 接口。

但是,随后发现了一个问题:在项目的其他业务场景下使用策略模式时,依然要创建一个“xxx策略工厂-xxxStrategyFactory”,依然要实现 Spring 的个接口,导致了重复代码的存在。

为了减少这种重复代码,我们在项目中,通过使用 ”泛型接口、注解、Spring的配置类“等方法,实现了多策略工厂的自动注入。

配置类1

从 “参考阅读2” 可以看出,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 值,比如 “参考阅读2” 中,每个 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 类都分别继承自这两个泛型接口:

  • 泛型 就是每个 Strategy 返回的 type 类型;
  • 因为其他的的 Strategy 接口都继承自 Strategy,所以 StrategyFactory 中addStrategy 方法参数、getByType 方法返回值,都是 Strategy。

项目中具体业务的 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 的子类,代码如下:

@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 并添加注解,就可以实现 StrategyFactory 和 Strategy 的自动注入。

上述代码在项目:github.com/ShiMengjie/… 的 work 模块中有演示。

总结

策略模式利用了多态特性,通过面向接口编程,隔离了 客户、Context 与具体策略实现类,降低了业务代码之间的耦合。

另外,为了避免在多次使用策略模式时,StrategyFactory 重复实现 InitializingBean, ApplicationContextAware 接口,可以使用范型接口、注解与配置 来减少重复代码。

附录. 参考阅读

1、Erich Gamma, Richard Helm, Ralph Johnson, Jojn Vlissides. 设计模式–可复用面向对象软件的基础[M]. 机械工业出版社,北京. 2018.11.

2、设计模式最佳套路—— 愉快地使用策略模式

原文链接:mp.weixin.qq.com/s/xRm4CuMKU…

今天的文章设计模式-策略模式分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20810.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注