ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

责任链模式实战:告别if-else,构建可扩展的处理链路

责任链模式实战:告别if-else,构建可扩展的处理链路 1. 先搞清楚责任链模式到底解决了什么问题1.1 从一段让人头疼的if-else说起我相信每个写业务代码超过半年的人都经历过这种场景一个请求从接口进来要先做参数校验再做用户权限校验然后查库存、风控拦截、最后落库。刚开始只有两三个判断if-else还能凑合写但随着业务方不断提需求校验项像滚雪球一样越滚越多代码就膨胀成了下面这个样子public void handleOrder(Order order) { if (!checkParam(order)) { throw new IllegalArgumentException(参数不合法); } if (!checkUserPermission(order.getUserId())) { throw new PermissionDeniedException(无权限操作); } if (!checkStock(order.getSkuId())) { throw new StockNotEnoughException(库存不足); } if (!riskControl(order)) { throw new RiskRejectedException(风控拦截); } // 继续处理业务... saveOrder(order); sendNotify(order); }这段代码看起来逻辑清晰甚至有人说“这不挺好理解的嘛”。但真实项目里问题在于每个校验方法内部可能有几十行复杂逻辑而且校验项之间还有优先级、可插拔、可配置的需求。今天产品说“新用户不校验风控”明天运营说“VIP用户跳过库存校验”后天风控说“金额大于十万需要额外人工审核”。这时候你再去改那段if-else每次都要小心翼翼生怕动了哪行影响到别的分支。更麻烦的是这些判断之间高度耦合代码复用性极差。下一个接口如果也要走类似的校验链路你只能复制粘贴一大段然后微调几行。一旦某个校验规则更新你得满项目地搜哪里复制过这段逻辑改漏一个就是线上事故。1.2 责任链的运作方式把“判断”拆成“传递”责任链模式Chain of Responsibility Pattern解决的就是这一类问题。它的核心思想非常朴素把每个判断逻辑封装成一个独立的处理器Handler多个处理器串成一条链请求从链头进入逐级传递直到有某个处理器能够处理它或者链路走完。你可以把它想象成公司里的报销审批流程你提交一笔报销先经过部门主管审批主管看金额不大就批了金额超过主管权限就自动转给总监总监权限也不够再转给总经理。整个过程中你不需要知道谁能批你的单子你只需要把单子交给第一个人剩下的事由这条审批链自己决定。对应到代码上每个处理器只需要关心两件事一是自己能不能处理这个请求二是如果处理不了就把请求传给链条上的下一个处理器。这种设计带来的直接好处是新增一种校验或者调整校验顺序不需要动已有的处理逻辑只需要改变链条的组装方式。我在实际项目里最常用到责任链的场景就是把一堆“前置校验”从业务主流程里剥离出来。主流程代码只负责核心业务各种校验、增强、兜底逻辑全部下沉到链路里每个处理器只干一件事。这样主流程的代码量直接少了一半而且每新增一种拦截规则新增一个处理器类就行基本不触碰原有代码符合开闭原则。1.3 责任链的三个核心角色责任链模式涉及的角色不多但每个角色都值得单独拎出来说清楚因为后面写代码的时候职责边界一旦模糊整个链路就会变得一团糟。第一个角色是抽象处理者Handler。它定义了一个处理请求的接口并且持有下一个处理者的引用。这个引用是形成“链”的关键通常用一个setNext方法或者构造器注入来组装。第二个角色是具体处理者ConcreteHandler。它实现了抽象处理者的接口在handleRequest方法里做两件事判断自己能否处理这个请求能处理就处理掉不能处理就调用下一个处理者的方法。第三个角色是请求对象Request。它封装了需要被处理的数据和上下文沿着链条一路传递下去。有些实现里还会带一个“处理结果”的对象记录每个环节的处理状态方便回溯。这三者之间的关系用一句话概括就是抽象处理者定义规范具体处理者实现逻辑请求对象充当载体。理解了这个基本骨架后面不管什么变体都是在这个基础上做文章。2. 用代码落地一个最小可运行的责任链实现2.1 抽象处理者先把骨架搭出来讲再多理论不如直接上代码。我用一个非常经典的订单处理场景来演示一个订单请求需要经过参数校验、用户校验、库存校验三道关卡全部通过才执行真正的下单逻辑。先定义抽象处理者这是整条链的骨架public abstract class OrderHandler { protected OrderHandler next; public void setNext(OrderHandler next) { this.next next; } // 模板方法定义处理流程子类只需实现doHandle public void handle(Order order) { if (doHandle(order)) { return; } if (next ! null) { next.handle(order); } } protected abstract boolean doHandle(Order order); }这里我做了个小设计handle方法是一个模板方法它固定了处理的流程框架——先调用当前处理器的doHandle如果当前处理器已经处理完毕返回true链就终止如果当前处理器处理不了返回false就把请求传给下一个处理器。子类只需要关心自己的doHandle逻辑就行不用重复写传递代码。返回值的含义也需要说清楚。在这个设计里我规定true表示“这个请求已经被当前处理器处理掉了不需要继续往下传”false表示“我处理不了传给下一个吧”。这个语义一定要在团队里约定好否则有人返回true表示“校验通过”有人返回true表示“校验失败”整个链路的走向就会完全混乱。2.2 具体处理者每个关卡只管自己的事接下来写三个具体处理者。每个类都只负责一道校验关卡通过就返回false把请求传下去不通过就直接抛出异常终止链路。参数校验处理器public class ParamCheckHandler extends OrderHandler { Override protected boolean doHandle(Order order) { if (order.getSkuId() null || order.getQuantity() 0) { throw new IllegalArgumentException(订单参数不合法); } System.out.println(参数校验通过); return false; } }用户权限校验处理器public class UserCheckHandler extends OrderHandler { Override protected boolean doHandle(Order order) { if (!userService.isVip(order.getUserId())) { throw new PermissionDeniedException(非VIP用户无权下单); } System.out.println(用户校验通过); return false; } }库存校验处理器public class StockCheckHandler extends OrderHandler { Override protected boolean doHandle(Order order) { if (stockService.getStock(order.getSkuId()) order.getQuantity()) { throw new StockNotEnoughException(库存不足); } System.out.println(库存校验通过); return false; } }注意看这里的代码量和if-else版本相比并没有减少太多而且好像还更多了。但关键区别在于每个处理器类都是独立的它们不知道彼此的存在也不依赖彼此的实现。你可以在任何需要的地方重复使用ParamCheckHandler而不用担心它和UserCheckHandler耦合在一起。2.3 链的组装与请求发起最后是客户端的使用方式。链的组装有两种常见写法第一种是挨个setNext第二种是构建一个链条管理器。先用最直观的挨个组装OrderHandler chain new ParamCheckHandler(); OrderHandler userHandler new UserCheckHandler(); OrderHandler stockHandler new StockCheckHandler(); chain.setNext(userHandler); userHandler.setNext(stockHandler); Order order new Order(sku_123, 2, 1001); chain.handle(order); System.out.println(下单成功);这段代码的阅读顺序就是从链头开始依次串联三个处理器。如果后面要加一个风控校验只需要新建一个RiskCheckHandler然后把它插到合适的位置比如插在库存校验之后OrderHandler riskHandler new RiskCheckHandler(); stockHandler.setNext(riskHandler);两行代码一个校验环节就加进去了原有的其他处理器一行都不用改。这种可插拔性在处理经常变动的业务规则时太重要了。2.4 用数组和循环动态构建更长的链当链上的处理器数量越来越多手动挨个setNext的方式会显得很啰嗦。更常见的做法是用一个数组或者List存储所有处理器然后循环构建链条public class OrderPipeline { private final ListOrderHandler handlers new ArrayList(); public OrderPipeline addHandler(OrderHandler handler) { handlers.add(handler); return this; } public void execute(Order order) { if (handlers.isEmpty()) { return; } for (int i 0; i handlers.size() - 1; i) { handlers.get(i).setNext(handlers.get(i 1)); } handlers.get(0).handle(order); } }然后客户端就可以非常优雅地组装链路OrderPipeline pipeline new OrderPipeline(); pipeline.addHandler(new ParamCheckHandler()) .addHandler(new UserCheckHandler()) .addHandler(new StockCheckHandler()) .addHandler(new RiskCheckHandler()); pipeline.execute(order);这种方式在Spring项目里尤其好用你可以把这些处理器都注册成Spring Bean然后用ListOrderHandler自动注入再通过Order注解控制顺序。新增处理逻辑的时候只需要写一个新类、加个注解连业务代码都不需要动。这个扩展思路我在后面的实战部分会再展开。3. 责任链模式的两种变体千万别搞混3.1 纯责任链与不纯责任链很多人学责任链模式的时候只记住了“沿着链传递直到有人处理”这一种形式但真实项目里还有一种更常用的变体两者行为差异很大。纯责任链模式的规则是链上的每个节点都只能选择“完全处理”或“完全传递”当一个节点处理了请求之后请求就不应该再往后面传了。就像一个客服电话转接到某个专员后问题就在那里解决不会再转给别人。不纯责任链模式则允许节点处理完自己的部分之后把请求继续往后传。每个节点更像是流水线上的一道工序都完成一部分加工然后传给下一道。这种模式在中间件、拦截器、过滤器里极其常见。比如Servlet规范里的Filter就是一个典型的不纯责任链。一个HTTP请求进来先经过编码过滤器再经过登录过滤器再经过日志过滤器每个过滤器只处理自己关注的那部分横切逻辑处理完就调用chain.doFilter()把请求继续往后传直到最终到达Servlet处理器。public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { request.setCharacterEncoding(UTF-8); // 处理完编码继续往下传 chain.doFilter(request, response); } }这两种变体没有谁优谁劣取决于业务场景。如果你是做“拦截”性质的校验一旦失败就该终止适合纯责任链如果你是做“增强”性质的处理每个环节都要参与适合不纯责任链。写代码之前先想清楚自己的场景属于哪一种否则代码实现容易走偏。3.2 与装饰器、观察者、策略模式的辨析责任链模式在行为型模式里不算难但初学者经常把它和另外几个模式搞混。我在这里做一次集中辨析以后面试或者设计系统的时候就不会被绕进去了。责任链 vs 装饰器模式。装饰器模式的核心是增强单个对象的功能每个装饰器都包着被装饰对象一层套一层执行的时候从外往内逐层调用。责任链的核心是请求沿着链传递每个节点是平级的节点之间通过next引用串联。装饰器重在对“对象”的功能叠加责任链重在对“请求”的流转分发。责任链 vs 观察者模式。观察者模式是一对多的广播关系一个事件发布所有订阅者都能收到通知彼此之间没有先后顺序的概念或者说顺序不影响结果。责任链是链式传递关系请求在同一时刻只会被一个节点处理处理不了才传给下一个有明确的先后顺序。责任链 vs 策略模式。策略模式是从多个算法中选择一个来执行客户端明确知道要选哪个策略选择权在客户端手里。责任链是请求发起方不需要知道最终由谁处理选择权在链路内部由各个节点自己决定是否处理。一张表格总结一下模式请求方向选择权节点关系典型场景责任链链式传递链路内部平级串联审批流、过滤器、校验链装饰器层层包裹客户端构造时确定套娃式嵌套IO流增强、缓存装饰观察者一对多广播订阅者自己决定是否响应互不依赖事件总线、消息通知策略多选一客户端指定相互独立支付方式、排序算法搞清楚这些区别你就能在正确的场景用正确的模式不至于项目里全是“拿着锤子看什么都是钉子”的混乱设计。4. 实战场景拆解我从哪几个地方用到了责任链4.1 场景一下单风控校验链路先说一个我参与过的电商项目。下单接口的业务规则极其复杂除了常规的参数校验、库存校验之外还有一堆营销规则限购校验、黑名单校验、地域限制校验、频次风控校验。不同业务线普通商品、秒杀商品、跨境商品的校验规则还不一样。如果用if-else写这个校验方法没有五百行根本下不来。而且运营经常临时调整规则今天加一个“新客首单免风控”明天加一个“大促期间放宽限购”。每改一次代码冲突、回归测试的成本都压得人喘不过气。后来我做了两个改造第一步把所有的校验逻辑抽成独立的Handler类每个类只负责一条规则第二步用Spring的依赖注入将所有的Handler按顺序组装成一个校验链通过配置类管理顺序。Configuration public class OrderCheckChainConfig { Bean public OrderChain orderChain(ListOrderCheckHandler handlers) { // Spring会自动把所有OrderCheckHandler的实现类按Order排序注入 handlers.sort(Comparator.comparingInt(OrderCheckHandler::getOrder)); OrderChain chain new OrderChain(); for (OrderCheckHandler handler : handlers) { chain.addHandler(handler); } return chain; } }每个具体的Handler加上Order注解控制执行顺序Component Order(1) public class ParamCheckHandler extends OrderCheckHandler { ... } Component Order(2) public class StockCheckHandler extends OrderCheckHandler { ... } Component Order(3) public class RiskCheckHandler extends OrderCheckHandler { ... }这样改完之后业务规则的增删改都变成了“新增一个类”或“调整一个注解”的动作再也不需要动主流程代码。而且每个Handler都可以单独写单元测试测试覆盖率和排错效率都大幅提升。这里我强烈建议如果你的校验链路已经有三个以上的环节并且有动态增删调整的需求尽早用责任链重构等if-else长到几百行再动手成本会高很多。4.2 场景二事件消息的多级兜底处理另一个典型场景是处理外部系统推送的消息。不同来源的消息格式不一样有些是JSON有些是XML有些甚至是不规范的文本。系统需要先尝试用JSON解析解析失败再用XML解析再不行就用正则提取关键字段实在处理不了就进入异常队列。这个场景天生适合责任链链上的每个节点尝试一种解析方式成功就处理掉请求并终止失败就传给下一个节点。比起写一串try-catch嵌套责任链的实现清晰得多而且新增一种解析格式时不需要改动已有代码。public abstract class MessageParser { protected MessageParser next; public abstract Message parse(MessageBody body); }每个解析器的逻辑顺手写一下大概是这个感觉public class JsonMessageParser extends MessageParser { Override public Message parse(MessageBody body) { try { return jsonMapper.readValue(body.getContent(), Message.class); } catch (Exception e) { if (next ! null) { return next.parse(body); } throw new UnsupportedMessageException(无法解析的消息格式: body.getContent()); } } }这种写法把每个解析逻辑彻底隔离开来不会出现“try-catch套try-catch”的恐怖结构。排查问题的时候只需要看是哪个解析器抛出的异常就能直接定位到对应的处理逻辑。4.3 场景三你天天都在用的责任链框架很多开发者没意识到一些常用框架的底层机制就带着责任链的影子。理解这些框架的设计对你的架构眼光提升很有帮助。第一个是Netty。Netty的Pipeline机制就是一条典型的不纯责任链。每个入站或出站的数据包都沿着ChannelPipeline传递经过一个个ChannelHandler每个Handler对数据做一部分处理解码、业务、编码然后通过fireChannelRead把数据传给下一个Handler。这种设计让Netty的扩展性极强你可以在Pipeline里任意插入自定义的日志、鉴权、流量控制Handler。第二个是Spring MVC的Interceptor。它的preHandle方法返回true请求继续向下传递给下一个拦截器返回false请求就中断在这里。默认情况下如果所有拦截器都放行请求才最终到达Controller。通过拦截器链登录检查、权限校验、CSRF防护都可以抽出来不必写在业务代码里。第三个是Java的异常处理机制。严格来说它不算标准的责任链实现但思想有相似之处异常沿着方法调用栈向上传播每层catch都可以决定是自己处理还是继续向上抛直到找到一个能处理它的catch块。了解这些框架的实现反过来也能帮助你加深对责任链模式的理解。你会发现责任链模式不只是一个考试知识点它在业界的应用已经非常广泛只是你可能没意识到那就是责任链。5. 实现责任链时最容易踩的坑与排查思路5.1 链路断裂请求默默失踪了责任链最经典的坑就是某个处理器既没有处理请求也没有把请求传给下一个节点导致请求“凭空消失”。这种情况一般表现在接口调用不报错但业务结果不对而且很难从日志里看出问题。排查思路很简单但需要提前做好准备每个节点都必须有日志输出或者埋点。我见过很多线上事故就是因为某个处理者在异常分支里忘记调用next.handle()请求到了它这里就断掉了。最简单的预防措施是在每个Handler的handle方法入口和出口都打日志记录请求状态和是否继续传递。责任链一长日志就是你的眼睛。另外我要特别提醒一下如果你用模板方法固定处理流程比如前面例子里的handle方法统一实现传递逻辑就能从架构层面避免这种问题。子类只负责返回boolean或抛出异常不用自己手写传递逻辑链条断裂的概率大幅降低。5.2 循环依赖与环形链另一个隐蔽的坑是链条成环。比如A的next指向BB的next指向C结果某次调整时不小心把C的next又指回了A请求就会在ABCA之间死循环直接导致栈溢出。这种问题在手工setNext时比较容易出现尤其是链上的节点多了以后。解决方案有三个一是用数组循环构建链条的方式从根上避免有人手动指错next二是在构建链表时记录节点数量如果超过预设上限就抛异常三是在handle方法里加一个计数器超过阈值说明可能存在循环直接终止执行。我个人的习惯是能用数组动态构建就绝不用手动setNext既能简化代码又能规避环形链风险。5.3 调试困难断点不知道下在哪个环节责任链的请求流转过程是隐式的不像if-else一眼能看到所有分支。调试的时候如果不知道当前请求走到哪个环节往往会浪费很多时间。实战里我的经验是两招配合使用。第一招是前面说过的每个环节入口出口打日志日志里带上请求的唯一ID比如订单号这样根据日志就能还原完整的处理轨迹。第二招是给每个Handler一个名字字段比如handlerName()打日志时直接输出名字排查速度飞快。如果用的是IDEA这类IDE还可以在抽象处理者的模板方法处下断点观察调用栈能看到完整的链式调用关系比一个个节点打断点高效得多。5.4 滥用责任链导致的过度设计责任链模式不是银弹它更适合“链路会扩展、规则会变动、节点之间有清晰的先后顺序”的场景。如果只是两三个固定的判断链路也不会怎么变硬造一个责任链出来反而增加类数量和代码理解成本。我这里有一个简单的判断标准个人实践总结供参考条件用if/else还是责任链校验环节超过3个责任链需要动态增删处理规则责任链每个处理环节逻辑复杂且独立责任链环节固定不变、逻辑简单if/else即可团队里代码风格偏保守、新人不熟模式如果链路不长别强行上说到底设计模式的目的是让代码更好维护而不是为了炫技增加理解负担。责任链是好东西但别把整个项目都堆成责任链。5.5 性能问题链越长开销越大最后说一个性能相关的坑。责任链的每一次传递都是一次方法调用如果链上有几十个节点请求每次都会经过完整的链路即使前几个节点已经确定可以拦截后面的节点也可能白白走一遍。这个坑在“不纯责任链”场景里尤为突出。解决思路有两个方向一是尽量让链保持在合理的长度建议最多控制在十个节点以内更长的链路要考虑拆分或合并节点二是对于可以提前终止的纯责任链一定要设计好短路机制让请求尽早返回。另外要注意在高并发场景下责任链中的每个节点都尽量不要做耗时的IO操作。如果某个校验确实需要远程调用建议做个批量查询或者加缓存避免串行调用把整体耗时拉爆。6. 责任链模式在真实项目中的扩展玩法6.1 结合Spring Boot Starter实现自动装配责任链模式在实际项目里最有价值的一种玩法是把它和Spring Boot的自动装配机制结合起来做成一个可插拔的starter。我参与过的一个支付网关项目里就采用了这种设计每种支付渠道支付宝、微信、银联、PayPal的请求校验逻辑各不相同但大体的校验流程是一致的。我们定义了一个抽象校验链然后通过Spring Boot的ConditionalOnProperty用配置开关控制哪些校验器生效。比如新接入一家银行渠道时只需要引入对应的校验器依赖并在配置里加一项开关无需修改核心系统代码。这种模式其实就是常说的扩展点设计而责任链正好是最自然的实现载体。对于中小型团队来说不一定非得做成Starter但至少可以在项目内建立一个“校验器扩展包”的目录规范新增的校验器类和配置放在统一位置形成一种可沉淀的扩展机制。6.2 链路节点支持动态排序与条件激活另一个非常实用的功能是让链路支持更细粒度的控制。单纯用Order固定顺序还是不够灵活我在项目中给责任链增加了一个“条件激活”机制每个Handler可以定义自己是否匹配当前请求只有匹配了才启用。实现方式很简单在抽象Handler里增加一个match(Request request)方法遍历链路时先判断match是否通过只有通过才执行对应的handle逻辑。这样一来同一个校验链可以同时服务多种业务线做到“同一套代码、不同规则组合”。public abstract class ConditionalHandler { public abstract boolean match(Order order); public abstract void handle(Order order); } public void execute(Order order) { for (ConditionalHandler handler : handlers) { if (handler.match(order)) { handler.handle(order); } } }这种实现本质上已经不依赖next指针了更像一个“可过滤的注册表”但责任链的思想依然贯穿其中将复杂流程拆分成独立的处理单元按一定规则依次执行。在业务规则多变的中大型项目里这种设计能极大地提升迭代效率。6.3 与AOP结合做细粒度的链路监控责任链的每个环节执行耗时、成功率、异常次数都是非常有价值的数据。如果能够统计到这些数据就可以快速定位链路瓶颈为优化提供依据。我推荐的做法是在抽象处理者里包一层AOP切面切入所有具体Handler的doHandle方法自动记录执行时间和结果。或者更简单一点用一个包装Handler把真正的业务Handler包起来前后埋点。不需要改动业务代码就能在整个链路上加上监控这在排障和性能优化时会给你极大的底气。要注意的是监控数据的维度建议至少包含链路名称、节点名称、请求标识、执行耗时、结果状态。存到日志或者时序数据库都行关键是得有这个数据不然链路一出问题就两眼一抹黑。我在实际使用中最大的体会是责任链模式真正的价值不在于代码量减少而在于让“变化点”有了固定的安放位置。每次业务方过来提新的校验规则我只需要新建一个类坐在工位上花五分钟写完然后跑一遍单元测试就提交了。不用再去翻那段几百行的if-else不用再担心改A影响B也不用再为了一个嵌套调用反复读代码。这份从容就是设计模式带来的回报。如果条件允许我建议你把责任链模式里“模板方法抽象处理者动态构建”的思路记牢不仅仅是用在Java里在Python、Go、Node.js里同样适用。它是一种思维范式而不是某种特定语言的一个语法糖。最后再分享一个小技巧新手学习责任链不要死记代码模板拿自己熟悉的业务场景改一遍比如请假审批、关单流程、消息处理管线改完再回头看理论你会发现自己是真的学会了。
返回列表