在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()
.Map(Strategy::flag, s -> s));
}
@Override
public void run(String flag) {
spring boot选择题(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()
.Map(Strategy::flag, s -> s));
return flag -> (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()
.Map(
// 获取策略标识
s -> s.getClass().getAnnotation(StrategyFlag.class).value(),
s -> s
));
return flag -> (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 = BeansWithAnnotation(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 -> (flag).apply(null);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取ApplicationContext容器对象
this.applicationContext = applicationContext;
}
}
这⾥的代码稍微有点复杂,但整体的思路还是很清晰的。
由于我们取消了接⼝,因此不能简单地通过参数注⼊来获取容器中地所有策略类,⽽只能通过ApplicationContext中的getBeansWithAnnotation来获取,所以StrategyConfig需要实现ApplicationContextAware来获取容器对象。
获取到所有策略类之后,需要遍历每个策略类的每个⽅法,⼀旦发现某个⽅法被标注了StrategyHandler接⼝,就放进strategyHandlers中,供将来调⽤,其中⽤到了⼀些Java反射的API。
总结
经过两次优化,我们基于SpringBoot实现了⼀个⽐较优雅的策略模式,这种⽅式⽆需实现任何接⼝,只需⼏个简单的注解就能声明式地指定策略类标识以及策略处理⽅法,可以很容易地扩展到各种需要使⽤策略模式的应⽤场景。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论