之前接手的项目中,有一次使用责任链模式重构项目模块的经历,在此记录。
项目是供应链相关服务,而其中的采购模块有一个根据库存,采购,销售和流转状况等数据分析和预测库存,达到对补货提供数据参考能力的模块。当然最初并没有这么多能力,模块最初设计只是作为库存数据可视化用途,在此后长达两年的周期内,不断进行功能迭代,每次的功能点都并不大,可能只是加个字段,多计算一点数据或是增加个计算逻辑,但是到我最后接手时,代码可读性已经很低。各个开发负责的代码风格迥异,没有统一规范,功能点实现分散且交叉在一起,梳理逻辑困难,历史需求没有文档留存,一些小的改动和细节已经不可知。
由于这个模块仍然是项目中比较重要的部分,持续的迭代是可预期的,为了提高模块的可维护性,对这个模块的重构也势在必行,不过也是因为太过重要,直接重构承受的风险太大,所以做了个分两步进行的长期重构计划,首先使用责任链模式重构模块内的结构,之后才是对代码逻辑的重新组合和修改。
网络上很多博客对于责任链模式的描述都是:
而根据很多责任链模式的实现来看,我认为责任链模式的定义其实是:
手上关于设计模式的书只有一本《Head First设计模式》来作为参考,书中对责任链模式夫人描述仅仅是一句话带过当你想要让一个以上的对象有机会能够处理某个请求的时候,就使用责任链模式,不过也已经足够佐证对责任链模式的定义了。
解耦,请求发送方和接收方解耦,并不会对处理逻辑进行强绑定
请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接
在给对象分派职责时,责任链可以给我们更多的灵活性,可以在运行时对该链进行动态的增删改,改变处理一个请求的职责
其实,责任链模式就是一种实现面向对象设计原钻里 高内聚,低耦合 思想的产物。
而在这个项目里使用的责任链更像是一个变种,因为目的并不是选取一个或多个链节点来执行,主要目的是对代码进行模块化的拆分。当然,拆分出来的某个节点的逻辑如果不需要被执行,也相当于跳过了这个节点。
接下来想讲一讲在这个项目里使用的责任链框架的实现。
设计思想就是类似常见的一些执行器的思想,将任务和执行分开,例如线程池。
所以在设计上分为两个大的部分,执行器和链节点
执行器:需要能够管理责任链,执行责任链逻辑,打印日志等一些功能。
链节点:需要定义注册方式,实现标准接口等。
既然是以线程池实现作为参考,我们先来看看线程池的任务是怎么定义的,通过观察源码可以发现,虽然execute方法里参数是Runnable这个接口类型,但是实际上都是会被包装成Thread对象来执行的。
所以在设计责任链的实现上可以也这样做,定义责任链执行方法接口,让责任链对象持有实现这个方法的实例,执行器方法里只需要接收接口就行了,当然相比起线程池这里多出了注册的步骤,可以通过使用注解的方式实现注册,减少对业务代码的侵入。
首先定义接口Processor,接口方法process,返回ProcessContext作为上下文,ProcessContext里面持有的是一个Map,用来存储上下文数据
public interface Processor {
ProcessorContext process(ProcessorContext context);
}
定义ChainNode类实现Processor接口,并定义一些通用属性和行为,持有一个Processor对象,与Thread持有Runnable对象类似,是代理模式的实现。
public class ChainNode implements Processor{
private Processor processor;
private ChainNode next;
public ChainNode(Processor processor){
this.processor = processor;
}
@Override
public ProcessorContext process(ProcessorContext context) {
return processor.process(context);
}
}
定义类ProcessChain作为执行器,或者命名为ChainExecutor也可以,这个类对象通过Map持有分组后的bean调用链
public class ChainExecutor {
//链对象列表
private Map> chainArrayList = new HashMap<>();
//链对象链表(需要自己定义一个头节点)
private Map chainLinkedList = new HashMap<>();
private Map processorMap;
public ChainExecutor(Map processorMap){
this.processorMap = processorMap;
init();
}
//省略部分代码
...
}
ChainRegister是注解,定义调用链中bean名称,分组,执行顺序
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ChainRegister {
//命名空间
String namespace() default "DEFAULT";
//名称
String name() default "VOID";
//顺序
int order() default 0;
}
而具体的执行节点需要实现Processor接口定义的方法,然后在类上添加注解
@Service
@ChainRegister(namespace = "TEST",name = "SAMPLE",order = 0)
public class SampleChainNode implements Processor{
@Override
public ProcessorContext process(ProcessorContext context) {
//具体方法实现
...
return context;
}
}
通过Configuration进行Bean注册,并且通过上下文获取被ChainRegister注解修饰的bean
@Configuration
@EnableAutoConfiguration
public class ChainConfig {
@Autowired
private ApplicationContext applicationContext;
@Bean
@ConditionalOnMissingBean
public ChainExecutor getChainProcessor(){
return new ChainExecutor(applicationContext.getBeansWithAnnotation(ChainRegister.class));
}
}
初始化时,读取bean注解的信息,确定初始化的分组和顺序
//初始化方法
private void init(){
for (Object p : processorMap.values()){
ChainRegister anno = p.getClass().getAnnotation(ChainRegister.class);
chainArrayList.compute(anno.namespace(),(key,value) -> {
if (value == null){
value = new ArrayList<>();
}
value.add((Processor) p);
return value;
});
}
//排序
for (List processorList : chainArrayList.values()){
Collections.sort(processorList,Comparator.comparing(p -> p.getClass().getAnnotation(ChainRegister.class).order()));
}
}
初始化时有两种策略可以选择,数组和链表。第一种是直接初始化成有序的责任链数组,执行器持有的map应该是Map
,第二种可以初始化成链表形式,由链表来维护顺序,需要在ChainProcessor里增加指向下一个节点的指针,在初始化时,将Processor类型的bean转换成ChainProcessor对象,并定义执行顺序,在Map
里只需要维护头节点。
执行时,通过名称获取调用链,然后循环执行并返回
@Test
void contextLoads() {
ProcessorContext context = chainExecutor.processArrayList("TEST",new HashMap<>());
System.out.println("执行结束");
System.out.println(context);
}
本文只介绍了一些关于责任链的基础知识和应用场景,基于spring实现了一个比较简单的责任链执行框架,更深入的内容,例如主流框架中责任链的代码实现暂时无力探索,而本文所写的责任链框架demo也还有许多值得改进和丰富的地方。