Spring 利用责任链模式优化代码

454 阅读5分钟

利用责任链模式优化代码

笔者自述:变动较多,愈发感觉生活无趣,好像只有代码能提起激情。

需求重现

前端传入四种题目,选择,判断,文字描述,后端需要根据题目的种类进行保存策略的变更。

有同学就会说:就这?几个if-else不就解决了?

于是有几个同学就会这么写:

if(type === '单选'){
//校验,保存数据
}else if(type === '判断'){
//校验,保存数据
}else if(type === '文字描述'){
//校验,保存数据
}

想象一下,如果是放在实际生产中的代码,这个代码会臃肿到什么程度,因为每个校验都非常复杂,到最后变得毫无拓展性,极其难维护。

什么是责任链模式?

责任链模式的本质,就是把一条链路拆分成很多节点。

说人话就是,分段办事。

这就好像,老师教课,学生听课,保安拦人一样,他们各司其职。

除了分段办事的特点,责任链模式还有一个特性就是过滤。

这就好像,豆浆从漏斗中穿过一样,豆渣留在了漏网上。

这么说可能有些抽象,我们来看看我们平时用到的责任链有关的东西。

最常见的应该就是Spring Cloud Gateway的写法了。

@Slf4j
@Component
@Order(1) // 最高优先级的过滤器
public class LoginFilter implements GlobalFilter {
​
​
    @Resource
    private StringRedisTemplate stringRedisTemplate;
​
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("=====进入登录鉴权网关拦截器=====");
        return chain.filter(exchange);
    }
}
@Slf4j
@Component
@Order(-1) // 最高优先级的过滤器
public class CustomGlobalFilter implements GlobalFilter {
​
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 请求日志
        ServerHttpRequest request =  exchange.getRequest();
        // 日志
        log.info("请求唯一标识:" + request.getId());
        log.info("请求路径:" + request.getPath().value());
        log.info("请求方法:" + request.getMethod());
        log.info("请求参数:" + request.getQueryParams());
        String sourceAddress = request.getLocalAddress().getHostString();
        log.info("请求来源地址:" + sourceAddress);
        log.info("请求来源地址:" + request.getRemoteAddress());
​
        // 2. 流量染色,证明是网关过来的(这一步改为声明式,写在配置文件里)
​
        // 3. 解析jwt
        // 过滤器中做
        return chain.filter(exchange);
    }
​

这是一个简单的示例。他做的其实就是对于进入网关的请求,进行层层校验筛选,进来的请求就是豆浆,被阻拦的就是豆渣。

每一个filter要做的事情也不一样,有的去对ip限制,有的去鉴权,有的去限制请求参数,有的负责转发请求,各司其职。

实战中实现

讲完了责任链模式的核心思想,下面我们来自己实现一个基于责任链模式的业务代码。

首先,我们需要对业务代码进行先后的排序,我这里的排序是这样的

校验题目 -> 保存文字描述 -> 校验选项 -> 保存单选,判断

为此,我们模仿上述代码实现三个Handler.

分别是 QuestionValidHandler , SaveTextHandler, OptionValidHandler,SaveChoiceHandler,对应四种情况。

之后我们定义一个接口去实现他们相同的行为。

这里,我写了一个BaseHandler , 然后让SaveOptionHandler这个接口去继承他。

BaseHandler

/**
 * 通用责任链的 Handler
 * 每个业务可以声明一个该接口的子接口或者抽象类,再基于该接口实现对应的业务Handler
 * @param <Param>
 * @param <Result>
 */
public interface BaseHandler<Param,Result> {
​
    /**
     * 定义的抽象方法,实现类需要去实现
     * @param param
     * @return
     */
    @NonNull
    HandleResult<Result> doHandle(Param param);
      /**
     * default修饰,让接口可以拥有接口的默认方法。只要实现该接口的类,都具有该默认方法,默认方法也可以被重写。
     * 这个是为了避免,每个实现类都要写重复的代码去实现相同的功能
     * @return
     */
    default boolean isHandler(Param param){
        return true;
    }
}
/**
 * @author ht
 */
public interface SaveOptionHandler extends BaseHandler<SaveOptionParam, Boolean> {
}
​

也就是说,上述的几个Handler,都必须要实现 HandleResult<Result> doHandle(Param param);

举个例子:我们用@Component 把这个handler注册成组件,@Order(0)来定义handler的执行先后。

/**
 * 测评设置是无维度或者题目是文字描述
 */
@Slf4j
@Order(0)
@Component
public class TextHandler implements SaveOptionHandler {
​
​
    @Override
    public @NonNull HandleResult<Boolean> doHandle(SaveOptionParam saveOptionParam) {
        //判断是否 测评设置是文字描述
        boolean isToNext = saveOptionParam.getQuestionSingleAddDto().getQuestionType().equals(QuestionTypeEnum.文字描述.getValue());
        if(!isToNext){
            return HandleResult.doNextResult();
        }
        //无需插入题目,无需给题目绑定维度。
        return HandleResult.doCurrentResult(true);
    }
}

虽然4个handler此时能处理各自要做的事情了,但是现在仍然缺少一个东西,去将他们连接起来。

说到连接,大家也许第一时间想到的是链表。没错,我们要基于链表去实现。

BaseHandlerChain

/**
 * 通用责任链模式
 * @author ht
 * @param <Handler>
 * @param <Param>
 * @param <Result>
 */
public class BaseHandlerChain<Handler extends BaseHandler<Param,Result>,Param,Result> {
    
    @Getter
    private final List<Handler> handlerList;
    
    
    public BaseHandlerChain(List<Handler> handlerList){
        this.handlerList = handlerList;
    }
    
    public Result handleChain(Param param){
        for (Handler handler : handlerList) {
            if(!handler.isHandler(param)){
                continue;
            }
            HandleResult<Result> result = handler.doHandle(param);
            if(result.isNext()){
                continue;
            }
            return result.getData();
        }
        return null;
    }
}

这是一个通用的责任链,handlerList存放所有的handler,构建链表。handleChain做了一件事:如果事情做完了,就把责任推给下一个handler。

然后我们再利用SaveOptionHandlerChain去继承BaseHandlerChain。

这里我们用 @Autowired把四个handler注入,这样就能把4个Component交给spring去管理了

@Service
public class SaveOptionHandlerChain extends BaseHandlerChain<SaveOptionHandler, SaveOptionParam ,Boolean> {
​
    /**
     * 将交给spring的实现类注入
     * @param saveOptionHandlerList
     */
    @Autowired
    public SaveOptionHandlerChain(List<SaveOptionHandler> saveOptionHandlerList) {
        super(saveOptionHandlerList);
    }
}

同时我们还需要一个东西,作为一个handler和下一个handler传递的媒介

HandlerResult

这里和上文的handleChain呼应,看不懂的同学对照着看

@Getter
public class HandleResult<R> {
​
    private final R data;
​
    private final boolean next;
​
    private HandleResult(R r,boolean next){
        this.data = r;
        this.next = next;
    }
​
    
    public static <R> HandleResult<R> doNextResult(){
        return new HandleResult<>(null,true);
    }
    
    public static <R> HandleResult<R> doCurrentResult(R r){
        return new HandleResult<>(r,false);
    }
}

使用

@Slf4j
@Order(0)
@Component
public class TextHandler implements SaveOptionHandler {
​
​
    @Override
    public @NonNull HandleResult<Boolean> doHandle(SaveOptionParam saveOptionParam) {
        //判断是否 测评设置是文字描述
        boolean isToNext = saveOptionParam.getQuestionSingleAddDto().getQuestionType().equals(QuestionTypeEnum.文字描述.getValue());
        if(!isToNext){
            return HandleResult.doNextResult();
        }
        //无需插入题目,无需给题目绑定维度。直接返回,不到下个责任链
        return HandleResult.doCurrentResult(true);
    }
}
​

Service中运用

@Resource
private SaveOptionHandlerChain saveOptionHandlerChain;