设计模式实战 -- 在Spring下设计实现简单的责任链模式框架

本文讲述了作者在接手一个供应链服务项目时,如何使用责任链模式对采购模块进行重构,以提高代码可维护性。责任链模式用于解耦请求发送方和接收方,实现高内聚低耦合。文中介绍了责任链模式的概念、优点和应用场景,并展示了基于Spring实现的责任链框架,包括执行器、链节点的设计和实现。最后,文章提到了责任链在Netty、SpringSecurity等框架中的应用,并总结了重构过程中的思考和经验。
摘要由CSDN通过智能技术生成

之前接手的项目中,有一次使用责任链模式重构项目模块的经历,在此记录。

背景

项目是供应链相关服务,而其中的采购模块有一个根据库存,采购,销售和流转状况等数据分析和预测库存,达到对补货提供数据参考能力的模块。当然最初并没有这么多能力,模块最初设计只是作为库存数据可视化用途,在此后长达两年的周期内,不断进行功能迭代,每次的功能点都并不大,可能只是加个字段,多计算一点数据或是增加个计算逻辑,但是到我最后接手时,代码可读性已经很低。各个开发负责的代码风格迥异,没有统一规范,功能点实现分散且交叉在一起,梳理逻辑困难,历史需求没有文档留存,一些小的改动和细节已经不可知。

由于这个模块仍然是项目中比较重要的部分,持续的迭代是可预期的,为了提高模块的可维护性,对这个模块的重构也势在必行,不过也是因为太过重要,直接重构承受的风险太大,所以做了个分两步进行的长期重构计划,首先使用责任链模式重构模块内的结构,之后才是对代码逻辑的重新组合和修改。

何为责任链(chain of responsibility)?

责任链模式定义

网络上很多博客对于责任链模式的描述都是:

  • 一个具体处理者对象只能在两个行为中选择一个:要么承担全部责任,要么将责任推给下家,不允许出现某一个具体处理者对象在承担了一部分或全部责任后又将责任向下传递的情况
  • 一个请求必须被某一个处理者对象所接收,不能出现某个请求未被任何一个处理者对象处理的情况

而根据很多责任链模式的实现来看,我认为责任链模式的定义其实是:

  • 允许某个请求被一个具体处理者部分处理后再向下传递或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求
  • 一个请求可以最终不被任何处理者对象所接收

手上关于设计模式的书只有一本《Head First设计模式》来作为参考,书中对责任链模式夫人描述仅仅是一句话带过当你想要让一个以上的对象有机会能够处理某个请求的时候,就使用责任链模式,不过也已经足够佐证对责任链模式的定义了。

职责链模式的主要优点

解耦,请求发送方和接收方解耦,并不会对处理逻辑进行强绑定

请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接

在给对象分派职责时,责任链可以给我们更多的灵活性,可以在运行时对该链进行动态的增删改,改变处理一个请求的职责

其实,责任链模式就是一种实现面向对象设计原钻里 高内聚,低耦合 思想的产物。

这里列举几个典型应用:
  • Netty 中的 Pipeline 和 ChannelHandler 通过责任链设计模式来组织代码逻辑
  • Spring Security 使用责任链模式,可以动态地添加或删除责任(处理 request 请求)
  • Spring AOP 通过责任链模式来管理 Advisor
  • Dubbo Filter 过滤器链也是用了责任链模式(链表),可以对方法调用做一些过滤处理,譬如超时(TimeoutFilter),异常(ExceptionFilter),Token(TokenFilter)等
  • Mybatis 中的 Plugin 机制使用了责任链模式,配置各种官方或者自定义的 Plugin,与 Filter 类似,可以在执行 Sql 语句的时候做一些操作

而在这个项目里使用的责任链更像是一个变种,因为目的并不是选取一个或多个链节点来执行,主要目的是对代码进行模块化的拆分。当然,拆分出来的某个节点的逻辑如果不需要被执行,也相当于跳过了这个节点。

责任链框架实现

接下来想讲一讲在这个项目里使用的责任链框架的实现。

设计

设计思想就是类似常见的一些执行器的思想,将任务和执行分开,例如线程池。

所以在设计上分为两个大的部分,执行器和链节点

执行器:需要能够管理责任链,执行责任链逻辑,打印日志等一些功能。

链节点:需要定义注册方式,实现标准接口等。

既然是以线程池实现作为参考,我们先来看看线程池的任务是怎么定义的,通过观察源码可以发现,虽然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<String, List<Processor>> chainArrayList = new HashMap<>();

    //链对象链表(需要自己定义一个头节点)
    private Map<String, ChainNode> chainLinkedList = new HashMap<>();

    private Map<String,Object> processorMap;

    public ChainExecutor(Map<String,Object> 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<Processor> processorList : chainArrayList.values()){
            Collections.sort(processorList,Comparator.comparing(p -> p.getClass().getAnnotation(ChainRegister.class).order()));
        }
    }

初始化时有两种策略可以选择,数组和链表。第一种是直接初始化成有序的责任链数组,执行器持有的map应该是Map<String,List<Processor>>,第二种可以初始化成链表形式,由链表来维护顺序,需要在ChainProcessor里增加指向下一个节点的指针,在初始化时,将Processor类型的bean转换成ChainProcessor对象,并定义执行顺序,在Map<String,ChainProcessor>里只需要维护头节点。

执行时,通过名称获取调用链,然后循环执行并返回

	@Test
	void contextLoads() {

		ProcessorContext context = chainExecutor.processArrayList("TEST",new HashMap<>());
		System.out.println("执行结束");
		System.out.println(context);
	}

结语

本文只介绍了一些关于责任链的基础知识和应用场景,基于spring实现了一个比较简单的责任链执行框架,更深入的内容,例如主流框架中责任链的代码实现暂时无力探索,而本文所写的责任链框架demo也还有许多值得改进和丰富的地方。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值