在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_MAP
的Map
来保存flag
和Strategy
实例之间的对应关系,static
块中的代码用于从STRATEGIES
列表构造STRATEGY_MAP
。这样,在run
方法中就可以很方便地获取到指定flag
的Strategy
实例。
这个实现虽然简单,但是它有个很大的缺点,想象一下,如果我们想添加新的Strategy
实现类,我们不仅需要添加新的实现类,还要修改STRATEGIES
列表的定义。这样就违反了“对扩展开放,对修改关闭”的原则。
在SpringBoot中实现策略模式
借助于Spring的IOC容器和SpringBoot的自动配置,我们可以以一种更加优雅的方式实现上述策略模式。
首先,我们继续使用StrategyImpl1
和StrategyImpl2
这两个实现类。不过,为了将它们注册进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
接口中有flag
和process
两个方法,实际上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");
}
}
这里的两个实现类已经被重命名成了Strategy1
和Strategy2
,而且它们并没有实现任何接口。实际上,这是两个互相独立的、互不相关的类,唯一的共同点是它们都被标注了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实现了一个比较优雅的策略模式,这种方式无需实现任何接口,只需几个简单的注解就能声明式地指定策略类标识以及策略处理方法,可以很容易地扩展到各种需要使用策略模式的应用场景。