ARTICLE DETAIL

资讯详情

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

设计模式 17 · 责任链模式

设计模式 17 · 责任链模式 上一篇观察者是一个事件广播给所有人、各自独立处理。这一篇的**责任链模式(Chain of Responsibility)**换一种协作方式:把多个处理者串成一条链,让请求沿着链一个一个往下传,由链上的节点依次处理——谁能处理就处理、谁想拦截就拦截。它的核心场景一句话:一个请求要经过一串关卡,每个关卡负责一项检查或处理,通过了才进入下一关,任何一关不通过就拦下。这就像你过机场安检:先查证件、再过安检门、再查行李、再核对登机牌,一道道关卡串成一条链,请求(你)顺着链往前走,每道关卡各管一段,任何一道拦下你就走不了。关键在于:每道关卡只管自己那一项,不知道整条链有多长、下一道是谁,它只负责处理完了把请求交给下一个。我们的订单场景里有个绝佳例子:下单前的风控校验链。一次下单请求,要依次经过一连串检查——库存是否充足、是否超过限购、用户是不是黑名单、是否触发风控规则……这些检查有先后、会增减(明天可能又要加个实名认证校验)。如果把它们全塞进一个方法里用if层层嵌套,那个方法会变成一个又深又乱、加一个检查就要动一次的泥潭。责任链模式,就是把每项检查做成一个独立的处理节点,串成一条链,请求顺着链走,清爽又可扩展。这篇文章按这条线索展开:先看一堆校验用嵌套 if 堆在一起的困境;再引出责任链如何把每项处理拆成链上的节点;然后讲清它的角色、以及链怎么组装、请求怎么流转这个关键;接着看它在 Servlet 过滤器、网关、拦截器里无处不在的身影;最后辨析它与其他模式、给出适用边界。贯穿例是下单风控链。目录一堆校验用嵌套 if 堆在一起的困境责任链模式:把处理者串成一条链角色,与链的组装和流转现实身影:过滤器、网关、拦截器责任链的两种形态,以及什么时候用一、一堆校验用嵌套 if 堆在一起的困境看下单前的风控校验。最直觉的写法,是在下单方法里把所有检查一项项if下来:publicclassOrderService{publicvoidplaceOrder(OrderRequestreq){// 1. 库存校验if(!inventory.enough(req)){thrownewRuntimeException(库存不足);}// 2. 限购校验if(purchase.exceedLimit(req)){thrownewRuntimeException(超过限购数量);}// 3. 黑名单校验if(blacklist.contains(req.getUserId())){thrownewRuntimeException(用户在黑名单);}// 4. 风控规则校验if(riskControl.hit(req)){thrownewRuntimeException(触发风控);}// ... 明天要加实名认证校验?继续往这堆doPlaceOrder(req);// 全过了才真正下单}}这段代码能跑,但毛病很典型:所有校验挤在一个方法里:placeOrder越来越长,库存、限购、黑名单、风控全糊在一起,违反单一职责。这个方法会长到没人愿意读。违反开闭原则:每加一项校验(实名认证、地址校验),都得回来撬开这个方法加一段if。改一次,就要重新测试整个下单流程。顺序和组合写死:校验的顺序被硬编码了。如果某个场景(比如内部测试下单)想跳过黑名单校验、或者调整校验顺序,你没法灵活配置,只能改代码。每项校验难以复用:库存校验这段逻辑,别的地方(比如加购物车)也想用,但它被埋在placeOrder里,拎不出来。问题的根源是:多个平行的处理步骤,被硬编码成了一长串固定的if。每一项校验本是独立的、可增减、可重排的,却被焊死在一个方法里。我们真正想要的是:把每一项校验做成一个独立的节点,像串珠子一样串成一条链;请求顺着链往下走,每个节点各管一项,通过就传给下一个、不通过就拦下。加校验就往链上加一个节点,调顺序就调节点的串接顺序。这就是责任链。二、责任链模式:把处理者串成一条链责任链的做法:定义一个抽象处理者,它持有下一个处理者的引用;每个具体处理者只做自己那一项处理,做完后把请求传给下一个;把这些处理者一个个串起来,就形成一条链。第一步,定义抽象处理者(关键是它持有next——指向下一个节点):publicabstractclassOrderHandler{protectedOrderHandlernext;// 指向链上的下一个处理者publicOrderHandlersetNext(OrderHandlernext){this.nextnext;returnnext;// 返回 next,方便链式组装}// 处理请求。做完自己的事,决定要不要传给下一个publicabstractvoidhandle(OrderRequestreq);// 一个小工具:把请求传给下一个节点(如果有)protectedvoidhandleNext(OrderRequestreq){if(next!null){next.handle(req);}// next 为 null,说明到链尾了,校验全部通过}}第二步,每项校验是一个具体处理者:publicclassInventoryHandlerextendsOrderHandler{publicvoidhandle(OrderRequestreq){if(!inventory.enough(req)){thrownewRuntimeException(库存不足);// 不通过,拦下,链中断}handleNext(req);// 通过,交给下一个节点}}publicclassLimitHandlerextendsOrderHandler{publicvoidhandle(OrderRequestreq){if(purchase.exceedLimit(req))thrownewRuntimeException(超过限购);handleNext(req);}}publicclassBlacklistHandlerextendsOrderHandler{publicvoidhandle(OrderRequestreq){if(blacklist.contains(req.getUserId()))thrownewRuntimeException(黑名单);handleNext(req);}}第三步,组装链,然后把请求丢给链头:// 组装:库存 → 限购 → 黑名单OrderHandlerchainnewInventoryHandler();chain.setNext(newLimitHandler()).setNext(newBlacklistHandler());// 请求从链头进入,自动顺着链走chain.handle(orderRequest);对比第一节,升级点非常清晰:每项校验被拆成一个独立的、可复用、可单独测试的节点;OrderService不再关心有哪些校验、什么顺序,它只管把请求丢给链头;要加实名认证校验,写个RealNameHandler串进链里就行;要调顺序,改组装顺序就行——都不用碰已有的校验节点。这就同时满足了单一职责和开闭原则。用一张图看这个请求沿链流转的结构最清楚:图里最该记住的,是那条请求顺着节点一路往下传的链,以及每个节点手里那个指向下一个的引用。每个节点只知道我的下一个是谁,不知道整条链的全貌——正是这种局部只关心下一步的设计,让链可以任意加长、任意重排,而节点本身不用改。这和链表的思想如出一辙。三、角色,与链的组装和流转责任链的角色很简洁,两个:角色本例中是谁职责抽象处理者(Handler)OrderHandler定义处理接口,持有next引用具体处理者(ConcreteHandler)InventoryHandler等处理自己负责的部分,决定是否传给下一个注意:发起请求的客户端通常也被算作一个隐含角色——它负责组装链、并把请求丢给链头。理解责任链,关键在于每个节点手里那个传不传给下一个的决定权。节点处理完自己的逻辑后,有两种选择:传下去:调用handleNext(req),把请求交给下一个节点继续处理(比如校验通过);不传:直接返回、或抛异常、或返回一个结果,中断整条链(比如校验不通过,后面的节点就不用走了)。这个传或不传的决定权在每个节点自己手里,正是责任链名字的由来——每个节点决定这个请求的责任,是我担下来(处理并中断),还是往后传。而这又引出两种不同的链的语义:一票否决式(如我们的风控校验):每个节点都必须通过,任何一个拦下就中断。请求要走完整条链才算成功。这是最常见的形态。找到能处理的就停式:请求沿链传递,直到遇到第一个有能力处理它的节点,处理完就结束,后面的不再走。比如审批流程——小额报销组长就批了、大额才往上传到总监,一旦有人批了就结束。两种语义结构一样,区别只在节点内部什么时候中断、什么时候传下去的逻辑。理解了这一点,你就能用同一套结构应对不同的业务。四、现实身影:过滤器、网关、拦截器责任链是框架里应用极广的模式,尤其在请求要经过一串处理的场景:Servlet 的 Filter(过滤器链):这是责任链最经典的实现。一个 HTTP 请求进来,要依次经过编码过滤器、鉴权过滤器、日志过滤器……每个Filter处理完调chain.doFilter()把请求传给下一个。你写一个自定义 Filter 加进链里,就能拦截所有请求做统一处理——加过滤器不动别的,正是责任链的开闭优势。Spring MVC 的拦截器(HandlerInterceptor):请求到达 Controller 前,依次经过一串拦截器(登录检查、权限检查等),也是责任链。网关的过滤器链(Spring Cloud Gateway、Zuul):API 网关里,请求要经过一长串过滤器(限流、鉴权、改写、路由……),是责任链在微服务层面的应用。Netty 的 ChannelPipeline:网络数据的编解码、业务处理,被组织成一条ChannelHandler的流水线,数据顺着流水线一站站处理,是责任链的高性能实现。OkHttp 的拦截器链:HTTP 请求/响应经过一串Interceptor,也是责任链。一个识别信号:凡是一个请求要依次经过一串可插拔的处理环节,尤其是名字里带 Filter、Interceptor、Pipeline、Chain 的,基本都是责任链。它是构建处理流水线的标准模式。五、责任链的两种形态,以及什么时候用补充一个实现上的细节:责任链有两种常见的组织形态:链表式(我们上面用的):每个节点自己持有next引用,自己负责调用下一个。结构纯粹,但组装稍繁琐。列表式(框架常用):不用每个节点持有next,而是把所有处理者放进一个List,由一个链的执行器按顺序遍历调用。Servlet 的FilterChain、很多框架的实现都是这种——更好管理、更容易动态增删和配置。两者思想一致,只是谁来驱动流转不同(节点自驱 vs 执行器驱动)。适合用责任链的信号:一个请求需要经过多个处理环节,这些环节会增减、顺序可能调整;你想让每个处理环节独立、可复用、可单独测试,并能灵活组装;发送者不需要知道到底谁会处理这个请求,只管丢给链头。不必用的信号:就固定两三个检查,永远不变——那直接顺序调用/几个if更直白,套责任链是过度设计;处理环节之间有复杂的相互依赖、需要来回交互——责任链是单向流水,不擅长这种,那可能是别的场景。几个注意点:链一定要有终点:要么某个节点处理并中断,要么请求走到链尾被妥善处理。别让请求走到链尾却没人处理还悄无声息——那会变成一个难查的 bug(请求消失了)。别把链搞得太长太隐蔽:链太长时,一个请求到底经过了哪些节点、在哪被拦下,排查起来会比较绕。保持链清晰、每个节点职责单一。判断的核心还是那句话:先确认真的存在一串会增减的、可独立处理的环节,责任链才值得上。给固定不变的两三步硬套责任链,凭空多出一堆 Handler 类和组装代码,是典型的过度设计。小结。责任链模式把多个处理者串成一条链,请求沿链传递,每个节点各管一段、决定处理并中断还是传给下一个,从而把一长串硬编码的if校验,拆成独立、可复用、可灵活组装的节点——新增或重排处理环节,不动已有节点。它有一票否决和找到能处理的就停两种语义,以及链表式和列表式两种组织形态。Servlet Filter、网关过滤器、Spring 拦截器、Netty Pipeline,都是它的身影,是构建处理流水线的标准模式。用它要注意链一定要有终点。下一篇我们讲状态模式——它和策略结构几乎一样,但意图相反:策略是平行地选一个算法,状态是对象随自身状态在不同行为间流转,典型如订单从待付款一步步走到已完成的状态机。
返回列表