利用责任链模式优化代码
笔者自述:变动较多,愈发感觉生活无趣,好像只有代码能提起激情。
需求重现
前端传入四种题目,选择,判断,文字描述,后端需要根据题目的种类进行保存策略的变更。
有同学就会说:就这?几个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;