在SpringBoot中优雅地实现策略模式

策略模式的简单实现

首先定义一个Strategy接口来表示一个策略:

public interface Strategy {
    String flag();
    void process();
}

其中flag方法返回当前策略的唯一标识,process则是该策略的具体执行逻辑。

下面是Strategy接口的两个实现类:

public class StrategyImpl1 implements Strategy {
    @Override
    public String flag() {
        return "s1";
    }

    @Override
    public void process() {
        System.out.println("strategy 1");
    }
}

public class StrategyImpl2 implements Strategy {
    @Override
    public String flag() {
        return "s2";
    }

    @Override
    public void process() {
        System.out.println("strategy 2");
    }
}

然后定义一个StrategyRunner接口用来表示策略的调度器:

public interface StrategyRunner {
    void run(String flag);
}

run方法内部通过判断flag的值来决定具体执行哪一个策略。

下面是一个简单的StrategyRunner

public class StrategyRunnerImpl implements StrategyRunner {
    private static final List<Strategy> STRATEGIES = Arrays.asList(new StrategyImpl1(), new StrategyImpl2());
    private static final Map<String, Strategy> STRATEGY_MAP;

    static {
        STRATEGY_MAP = STRATEGIES.stream()
                .collect(Collectors.toMap(Strategy::flag, s -> s));
    }

    @Override
    public void run(String flag) {
        STRATEGY_MAP.get(flag).process();
    }
}

StrategyRunnerImpl内部,定义了一个STRATEGIES列表来保存所有Strategy实现类的实例,以及一个叫做STRATEGY_MAPMap来保存flagStrategy实例之间的对应关系,static块中的代码用于从STRATEGIES列表构造STRATEGY_MAP。这样,在run方法中就可以很方便地获取到指定flagStrategy实例。

这个实现虽然简单,但是它有个很大的缺点,想象一下,如果我们想添加新的Strategy实现类,我们不仅需要添加新的实现类,还要修改STRATEGIES列表的定义。这样就违反了“对扩展开放,对修改关闭”的原则。

在SpringBoot中实现策略模式

借助于Spring的IOC容器和SpringBoot的自动配置,我们可以以一种更加优雅的方式实现上述策略模式。

首先,我们继续使用StrategyImpl1StrategyImpl2这两个实现类。不过,为了将它们注册进Spring的IOC容器,需要给他们标注上Component注解:

@Component
public class StrategyImpl1 implements Strategy {
    ...
}

@Component
public class StrategyImpl2 implements Strategy {
    ...
}

然后,写一个StrategyConfig配置类,用于向容器中注册一个StrategyRunner

@Configuration
public class StrategyConfig {
    @Bean
    public StrategyRunner strategyRunner(List<Strategy> strategies) {
        Map<String, Strategy> strategyMap = strategies.stream()
                .collect(Collectors.toMap(Strategy::flag, s -> s));
        return flag -> strategyMap.get(flag).process();
    }
}

仔细看strategyRunner方法的实现,不难发现,其中的逻辑与之前的StrategyRunnerImpl几乎完全相同,也是根据一个List<Strategy>来构造一个Map<String, Strategy>。只不过,这里的strategies列表不是我们自己构造的,而是通过方法参数传进来的。由于strategyRunner标注了Bean注解,因此参数上的List<Strategy>实际上是在SpringBoot初始化过程中从容器获取的,所以我们之前向容器中注册的那两个实现类会在这里被注入。

这样,我们再也无需操心系统中一共有多少个Strategy实现类,因为SpringBoot的自动配置会帮我们自动发现所有实现类。我们只需编写自己的Strategy实现类,然后将它注册进容器,并在任何需要的地方注入StrategyRunner

@Autowired
private StrategyRunner strategyRunner;

然后直接使用strategyRunner就行了:

strategyRunner.run("s1");
strategyRunner.run("s2");

控制台输出如下:

strategy 1
strategy 2

也就是说,当我们想添加新的Strategy实现类时,直接向容器中注册就行,Spring会自动帮我们“发现”这些策略类,这样就完美地实现了“对扩展开放,对修改关闭”的目标。

进一步优化

到这里,其实还是有优化的空间。我们看到Strategy接口中有flagprocess两个方法,实际上flag方法完全可以用注解来指定,这样Strategy接口中就只剩一个方法,看起来更清爽。

首先定义一个StrategyFlag注解,用来指定策略的标识:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface StrategyFlag {
    String value();
}

然后删除Strategy接口中的flag方法,同时改造两个实现类:

public interface Strategy {
    void process();
}

@StrategyFlag("s1")
public class StrategyImpl1 implements Strategy {
    @Override
    public void process() {
        System.out.println("strategy 1");
    }
}

@StrategyFlag("s2")
public class StrategyImpl2 implements Strategy {
    @Override
    public void process() {
        System.out.println("strategy 2");
    }
}

注意,StrategyFlag注解被Component注解标注了,所以打了StrategyFlag注解的类同时也打了Component注解,因此也会被注册到容器中。

最后修改StrategyConfig的实现:

@Bean
public StrategyRunner strategyRunner(List<Strategy> strategies) {
    Map<String, Strategy> strategyMap = strategies.stream()
        .collect(Collectors.toMap(
            // 获取策略标识
            s -> s.getClass().getAnnotation(StrategyFlag.class).value(), 
            s -> s
        ));
    return flag -> strategyMap.get(flag).process();
}

这里与上一版本的代码基本相同,唯一不同的是s.getClass().getAnnotation(StrategyFlag.class).value()这行代码,通过解析策略类上标注的StrategyFlag注解来获取策略的标识。

更进一步优化

到这里,其实已经非常优雅了,我们用一个注解不仅换来了更好的可读性,还减少了一个接口方法。

不过,仔细思考一下,每个策略类都会被标注StrategyFlag注解,所以理论上仅仅依靠StrategyFlag注解就能发现所有的策略类,也就是说,Strategy接口已经不再需要了,所以我们可以大胆删掉Strategy接口。

但是问题来了,假如没有了Strategy接口,那要如何确定具体策略的处理函数呢?

这里有两种方法来实现,一种是依靠自定义注解,另一种是通过解析方法签名。由于通过自定义注解来实现比较简单,所以下面演示一下这种方法。

首先创建一个自定义注解StrategyHandler

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface StrategyHandler {
}

这个注解主要用来标注在方法上,用来标识策略的处理方法。

然后改造两个策略实现类:

@StrategyFlag("s1")
public class Strategy1 {
    @StrategyHandler
    public void handleStrategy1() {
        System.out.println("strategy 1");
    }
}

@StrategyFlag("s2")
public class Strategy2 {
    @StrategyHandler
    public void handleStrategy2() {
        System.out.println("strategy 2");
    }
}

这里的两个实现类已经被重命名成了Strategy1Strategy2,而且它们并没有实现任何接口。实际上,这是两个互相独立的、互不相关的类,唯一的共同点是它们都被标注了StrategyFlag注解,都包含一个标注了StrategyHandler接口的方法,而且这个方法是无参数、无返回值的。

接下来是StrategyConfig的实现:

@Configuration
public class StrategyConfig implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Bean
    public StrategyRunner strategyRunner() {
        // 从容器中获取所有标注了StrategyFlag注解的类
        Map<String, Object> strategyClass = applicationContext.getBeansWithAnnotation(StrategyFlag.class);

        // 策略处理函数
        Map<String, Function<Void, Void>> strategyHandlers = new HashMap<>();

        // 遍历所有策略类
        for (Object s : strategyClass.values()) {
            // 获取策略标识
            String flag = s.getClass().getAnnotation(StrategyFlag.class).value();

            // 遍历策略类中的所有方法
            for (Method m : s.getClass().getMethods()) {
                // 如果方法标注了StrategyHandler注解,则封装成可调用对象加入strategyHandlers
                if (m.isAnnotationPresent(StrategyHandler.class)) {
                    strategyHandlers.put(flag, ignored -> {
                        try {
                            m.invoke(s);
                            return null;
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    });
                    break;
                }
            }
        }

        return flag -> strategyHandlers.get(flag).apply(null);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取ApplicationContext容器对象
        this.applicationContext = applicationContext;
    }
}

这里的代码稍微有点复杂,但整体的思路还是很清晰的。

由于我们取消了接口,因此不能简单地通过参数注入来获取容器中地所有策略类,而只能通过ApplicationContext中的getBeansWithAnnotation来获取,所以StrategyConfig需要实现ApplicationContextAware来获取容器对象。

获取到所有策略类之后,需要遍历每个策略类的每个方法,一旦发现某个方法被标注了StrategyHandler接口,就放进strategyHandlers中,供将来调用,其中用到了一些Java反射的API。

总结

经过两次优化,我们基于SpringBoot实现了一个比较优雅的策略模式,这种方式无需实现任何接口,只需几个简单的注解就能声明式地指定策略类标识以及策略处理方法,可以很容易地扩展到各种需要使用策略模式的应用场景。

posted @ 2021-06-11 08:04  baiyuxuan  阅读(970)  评论(0编辑  收藏  举报