责任链模式:多级校验、多级处理如何优雅串起来
上一篇讲了模板方法模式。它解决的问题是当一个业务流程整体固定但其中某些步骤会变化时如何复用流程骨架并把变化点交给子类实现。比如诊断服务处理流程、远程控制执行流程、OTA 安装流程、自动化测试流程都可以通过模板方法把公共流程固定下来。模板方法模式的重点是父类固定流程子类实现变化步骤。这一篇讲责任链模式。它解决的问题是当一个请求需要经过多级校验、多级处理且每一级都有机会决定继续、拦截或处理时应该怎么设计。车企软件里这种场景非常多远程车控指令需要经过用户权限、车辆在线、车辆状态、风控、指令执行等多级处理诊断请求需要经过会话校验、安全访问、参数校验、服务分发、响应构造OTA 升级前需要经过电量、档位、车速、网络、版本、空间等多级检查告警事件需要经过过滤、去重、分级、抑制、上报等多级处理数据上报需要经过采集、清洗、压缩、脱敏、缓存、上传测试用例执行前需要经过环境检查、资源检查、前置条件检查、执行、后置清理这些场景有一个共同点一个请求不是一步完成而是要沿着一条处理链路逐级往下走。如果全部写在一个大函数里代码很快会变成一坨顺序判断。责任链模式就是用来解决这类问题的。一、先从一个车企场景说起假设我们现在做一个远程车控模块。用户在 App 上发起远程开空调指令。这个指令不能直接执行。它至少要经过这些检查用户是否登录 ↓ 用户是否有车辆控制权限 ↓ 车辆是否在线 ↓ 车辆是否处于允许控制状态 ↓ 是否触发频控或风控 ↓ 指令是否可以下发 ↓ 执行具体车控动作如果直接写代码可能是这样RemoteResultRemoteControlService::Handle(constRemoteCommandcommand){if(!authService_-IsLogin(command.userId)){returnRemoteResult::Fail(user not login);}if(!permissionService_-CanControl(command.userId,command.vin)){returnRemoteResult::Fail(no permission);}if(!vehicleService_-IsOnline(command.vin)){returnRemoteResult::Fail(vehicle offline);}if(!vehicleStateService_-CanExecute(command.vin,command.type)){returnRemoteResult::Fail(vehicle state not allowed);}if(riskService_-IsBlocked(command.userId,command.vin)){returnRemoteResult::Fail(risk blocked);}returnexecutor_-Execute(command);}这段代码能不能用能。而且一开始看起来还挺清楚。但随着业务变化问题会越来越明显。比如后来要加设备绑定校验 车辆区域限制 账号风控 夜间指令限制 节假日策略 不同车型差异 指令幂等校验 审计日志 灰度开关这个函数就会不断变长。最后它会变成if(...)return...if(...)return...if(...)return...if(...)return...if(...)return...每增加一级校验都要修改核心处理函数。每调整顺序也要改这个函数。每个检查逻辑本来是独立的最后却全部挤在一起。真正的问题不是if本身不好。而是多级处理逻辑被硬编码在一个函数里导致链路难扩展、难复用、难调整。责任链模式要做的事情就是把每一级处理封装成独立处理器让请求沿着处理器链路逐级传递。二、为什么多级校验代码容易写乱多级校验、多级处理在工程里特别常见。但它也特别容易写乱。1. 处理步骤会不断增加一开始可能只有三步权限校验 状态校验 执行指令后来会变成登录校验 权限校验 绑定校验 车辆在线校验 车辆状态校验 频控校验 风控校验 幂等校验 审计日志 指令执行 结果上报每一步都不复杂。但全部堆在一个函数里就会很复杂。2. 每一步的变化频率不同权限校验可能由账号体系决定。车辆状态校验可能由车控策略决定。风控校验可能由运营策略决定。幂等校验可能由服务端架构决定。指令执行可能由车端协议决定。这些步骤的变化来源不同。如果全部写在一个函数里任何一个方向变化都要修改同一个核心函数。3. 顺序很重要多级处理不是随便排的。比如先校验权限再执行指令 先检查车辆状态再下发控制 先做幂等判断再真正执行 先过滤告警再上报云端如果顺序写散了很容易出问题。责任链可以把顺序集中到链路组装处让链路结构更清楚。4. 某一步可能中断后续处理比如用户没权限后面不用检查车辆状态 车辆不在线后面不用执行指令 风控拦截后面不能下发命令 参数不合法后面不能进入业务逻辑也就是说链路中的每个节点都有机会决定继续往下走 或者到此为止这正是责任链模式的典型特征。5. 某些步骤希望复用比如车辆在线校验不只远程开空调用。远程解锁、远程闭锁、远程寻车、远程开窗也都可能需要。如果每个业务都复制一份检查逻辑后面维护会很痛苦。责任链可以把这些检查步骤做成可复用处理器。三、责任链模式到底是什么责任链模式可以这样理解把多个处理对象连成一条链让请求沿着这条链传递直到被处理、被拦截或者传到链尾。再说得直白一点不要让一个大函数负责所有判断而是让每个节点只负责自己那一关。比如远程车控指令可以拆成LoginCheckHandler ↓ PermissionCheckHandler ↓ VehicleOnlineCheckHandler ↓ VehicleStateCheckHandler ↓ RiskCheckHandler ↓ CommandExecuteHandler每个 Handler 只关心一件事。登录校验只关心登录。权限校验只关心权限。车辆在线校验只关心在线状态。风控校验只关心是否拦截。执行器只关心真正执行指令。请求从链头进入。如果某一级通过就交给下一级。如果某一级失败就直接返回结果。这就是责任链模式的核心。四、责任链模式解决的核心问题1. 把多级处理拆成独立节点没有责任链时代码可能是if(!CheckA())return;if(!CheckB())return;if(!CheckC())return;DoBusiness();有了责任链后变成AHandler - BHandler - CHandler - BusinessHandler每个节点只负责自己的逻辑。2. 让链路顺序更清晰责任链把处理顺序显式表达出来login-SetNext(permission)-SetNext(vehicleOnline)-SetNext(vehicleState)-SetNext(risk)-SetNext(executor);读代码的人可以直接看出请求先经过谁再经过谁最后由谁执行。这比在一个大函数里找几十个if更清楚。3. 新增处理步骤更容易比如要新增一个“频控校验”。只需要新增一个 HandlerRateLimitHandler然后插到链路中PermissionCheckHandler ↓ RateLimitHandler ↓ VehicleOnlineCheckHandler原来的 Handler 不需要改。4. 支持中途拦截每个处理器都可以决定通过继续往下 失败直接返回 已处理结束链路这很适合权限校验、参数校验、风控拦截、告警过滤这类场景。5. 处理器可以复用和组合同一个VehicleOnlineCheckHandler可以用于远程解锁 远程闭锁 远程开空调 远程寻车 远程开窗不同业务可以组装不同链路。比如远程开空调 登录 - 权限 - 在线 - 车辆状态 - 风控 - 执行 远程寻车 登录 - 权限 - 在线 - 频控 - 执行 远程闭锁 登录 - 权限 - 在线 - 车门状态 - 执行这样比复制粘贴一堆校验逻辑更可维护。五、责任链模式的核心角色责任链模式通常有几个核心角色。1. Handler抽象处理器定义统一处理接口。比如Handle(context)同时持有下一个处理器。如果当前节点处理完还需要继续就调用下一个处理器。2. ConcreteHandler具体处理器每个具体处理器负责一个明确职责。比如LoginCheckHandler PermissionCheckHandler VehicleOnlineCheckHandler RiskCheckHandler CommandExecuteHandler它们都实现统一的 Handler 接口。3. Context请求上下文请求在链路上传递时通常需要一个上下文对象。比如远程车控上下文里可能有用户 ID 车辆 VIN 指令类型 车辆状态 请求参数 链路日志 中间结果每个 Handler 可以读取或补充上下文。4. Chain Builder链路组装者负责把多个 Handler 按顺序串起来。链路可以写死。也可以根据配置、车型、业务类型动态组装。六、责任链模式的结构一个简化结构可以这样理解Client ↓ Handler ↓ next Handler ↓ next Handler ↓ next Handler请求从第一个 Handler 进入。每个 Handler 做三件事之一1. 自己处理然后结束 2. 自己处理一部分然后交给下一个 3. 判断不通过直接拦截返回在远程车控里更常见的是第二种和第三种校验通过 - 继续 校验失败 - 中断七、用 C 实现一个责任链我们还是用远程车控来举例。先定义请求和结果#includeiostream#includememory#includestringenumclassCommandType{Unlock,Lock,StartAc,StopAc,FindCar};structRemoteCommandContext{std::string userId;std::string vin;CommandType type;booluserLoggedInfalse;boolhasPermissionfalse;boolvehicleOnlinefalse;boolvehicleStateAllowedfalse;boolriskBlockedfalse;};structRemoteResult{boolsuccess;std::string message;staticRemoteResultOk(conststd::stringmsgok){return{true,msg};}staticRemoteResultFail(conststd::stringmsg){return{false,msg};}};然后定义抽象处理器classRemoteCommandHandler{public:virtual~RemoteCommandHandler()default;std::shared_ptrRemoteCommandHandlerSetNext(std::shared_ptrRemoteCommandHandlernext){next_next;returnnext_;}RemoteResultHandle(RemoteCommandContextcontext){autoresultDoHandle(context);if(!result.success){returnresult;}if(next_){returnnext_-Handle(context);}returnresult;}protected:virtualRemoteResultDoHandle(RemoteCommandContextcontext)0;private:std::shared_ptrRemoteCommandHandlernext_;};这里的关键是DoHandle(context)只负责当前节点自己的逻辑。如果当前节点失败就直接返回。如果成功并且有下一个节点就继续传递。八、实现几个具体 Handler1. 登录校验classLoginCheckHandler:publicRemoteCommandHandler{protected:RemoteResultDoHandle(RemoteCommandContextcontext)override{if(!context.userLoggedIn){returnRemoteResult::Fail(user not login);}std::coutlogin check passed\n;returnRemoteResult::Ok();}};2. 权限校验classPermissionCheckHandler:publicRemoteCommandHandler{protected:RemoteResultDoHandle(RemoteCommandContextcontext)override{if(!context.hasPermission){returnRemoteResult::Fail(no vehicle control permission);}std::coutpermission check passed\n;returnRemoteResult::Ok();}};3. 车辆在线校验classVehicleOnlineCheckHandler:publicRemoteCommandHandler{protected:RemoteResultDoHandle(RemoteCommandContextcontext)override{if(!context.vehicleOnline){returnRemoteResult::Fail(vehicle offline);}std::coutvehicle online check passed\n;returnRemoteResult::Ok();}};4. 车辆状态校验classVehicleStateCheckHandler:publicRemoteCommandHandler{protected:RemoteResultDoHandle(RemoteCommandContextcontext)override{if(!context.vehicleStateAllowed){returnRemoteResult::Fail(vehicle state not allowed);}std::coutvehicle state check passed\n;returnRemoteResult::Ok();}};5. 风控校验classRiskCheckHandler:publicRemoteCommandHandler{protected:RemoteResultDoHandle(RemoteCommandContextcontext)override{if(context.riskBlocked){returnRemoteResult::Fail(risk control blocked);}std::coutrisk check passed\n;returnRemoteResult::Ok();}};6. 指令执行classCommandExecuteHandler:publicRemoteCommandHandler{protected:RemoteResultDoHandle(RemoteCommandContextcontext)override{std::coutexecute remote command\n;switch(context.type){caseCommandType::Unlock:returnRemoteResult::Ok(unlock success);caseCommandType::Lock:returnRemoteResult::Ok(lock success);caseCommandType::StartAc:returnRemoteResult::Ok(start ac success);caseCommandType::StopAc:returnRemoteResult::Ok(stop ac success);caseCommandType::FindCar:returnRemoteResult::Ok(find car success);}returnRemoteResult::Fail(unknown command);}};九、把处理器串成一条链调用方可以这样组装链路intmain(){autologinstd::make_sharedLoginCheckHandler();autopermissionstd::make_sharedPermissionCheckHandler();autoonlinestd::make_sharedVehicleOnlineCheckHandler();autostatestd::make_sharedVehicleStateCheckHandler();autoriskstd::make_sharedRiskCheckHandler();autoexecutorstd::make_sharedCommandExecuteHandler();login-SetNext(permission)-SetNext(online)-SetNext(state)-SetNext(risk)-SetNext(executor);RemoteCommandContext context;context.userIduser_001;context.vinVIN123456;context.typeCommandType::StartAc;context.userLoggedIntrue;context.hasPermissiontrue;context.vehicleOnlinetrue;context.vehicleStateAllowedtrue;context.riskBlockedfalse;autoresultlogin-Handle(context);std::coutresult: result.messagestd::endl;return0;}这样请求会按顺序经过LoginCheckHandler ↓ PermissionCheckHandler ↓ VehicleOnlineCheckHandler ↓ VehicleStateCheckHandler ↓ RiskCheckHandler ↓ CommandExecuteHandler如果任何一个节点失败后面的节点就不会再执行。这就是责任链的基本用法。十、责任链有两种常见用法很多人学责任链时容易只记住一种一个请求沿着链传递直到某个 Handler 处理它。这确实是经典责任链。但工程里更常见的其实有两类。1. 分派型责任链这种链路里请求会沿着链找“谁能处理我”。比如诊断服务分发ReadDidHandler ↓ WriteDidHandler ↓ RoutineControlHandler ↓ ClearDtcHandler每个 Handler 判断我能不能处理这个 SID能处理就处理。不能处理就交给下一个。这种适合多个处理器中只有一个最终处理请求。2. 流水线型责任链这种链路里每个节点都要处理一部分。比如远程车控登录校验 ↓ 权限校验 ↓ 车辆状态校验 ↓ 风控校验 ↓ 执行指令每个节点都参与。任意节点失败都可以中断。这种更像工程里的拦截器、过滤器、处理流水线。适合多个处理步骤按顺序协作完成请求。公众号里讲责任链我更建议你重点记住第二种。因为真实业务里多级校验、多级拦截、多级处理特别常见。十一、再看一个诊断请求例子诊断服务也很适合责任链。比如一个 UDS 请求进来后可能要经过基础格式校验 ↓ 会话权限校验 ↓ 安全访问校验 ↓ 参数校验 ↓ 具体服务处理 ↓ 响应构造可以设计成RequestFormatHandler SessionCheckHandler SecurityCheckHandler ParameterCheckHandler ServiceDispatchHandler ResponseBuildHandler这样每一级职责很清楚。比如SessionCheckHandler只关心当前诊断会话是否允许执行这个服务SecurityCheckHandler只关心当前安全等级是否满足要求ParameterCheckHandler只关心请求参数是否合法ServiceDispatchHandler才关心这个请求到底是 ReadDID、WriteDID还是 RoutineControl这种拆法比把所有判断写在HandleRequest()里更容易维护。十二、责任链和模板方法有什么区别上一篇刚讲模板方法很容易和责任链混在一起。它们都可以处理“多个步骤”。但重点不一样。模板方法关注流程骨架模板方法适合这种场景流程固定 步骤固定 顺序固定 部分步骤由子类实现比如检查权限 - 校验参数 - 执行业务 - 构造响应 - 记录日志父类规定流程。子类补充某些步骤。责任链关注处理节点的动态组合责任链适合这种场景有多个独立处理节点 节点可以增减 顺序可以调整 节点可以决定是否继续比如登录校验 - 权限校验 - 风控校验 - 执行指令这些节点不一定要通过继承绑定在一个父类流程里。它们可以灵活组合成不同链路。一句话区分模板方法是流程由父类固定变化点由子类实现。责任链是请求沿着处理器链传递每个节点决定处理和是否继续。如果流程非常稳定模板方法更合适。如果处理节点经常增减、重排、组合责任链更合适。十三、责任链和装饰器有什么区别责任链和装饰器结构上也有点像。都是一个对象持有另一个同类接口对象但它们的目的不同。装饰器是增强对象能力装饰器关注的是在不修改原对象的情况下给它动态增加能力。比如通信模块 ↓ 加日志 ↓ 加重试 ↓ 加限流装饰器通常最终还是为了调用核心对象。责任链是传递请求责任链关注的是请求在多个处理器之间传递每个处理器有机会处理或中断。比如权限校验 ↓ 状态校验 ↓ 风控校验 ↓ 执行指令每个节点不是单纯增强同一个对象而是在承担不同责任。一句话区分装饰器强调给对象一层层加能力。责任链强调让请求一站一站往下传。十四、责任链和策略模式有什么区别策略模式通常是在多个算法里选一个。比如EcoRegenStrategy ComfortRegenStrategy SportRegenStrategy运行时选其中一个来执行。责任链不是选一个算法。责任链是多个处理器按顺序协作。比如PermissionHandler VehicleStateHandler RiskHandler ExecuteHandler它们不是互斥关系。它们是链路关系。一句话区分策略模式是“选一个来做”责任链是“按顺序交给多个节点处理”。十五、责任链适合哪些场景责任链特别适合这些场景。1. 多级校验比如权限校验 参数校验 状态校验 风控校验 幂等校验只要有一级失败就中断后续处理。2. 多级过滤比如告警处理原始告警 ↓ 无效告警过滤 ↓ 重复告警过滤 ↓ 低优先级抑制 ↓ 告警分级 ↓ 上报每一级只负责一种过滤或加工。3. 请求拦截器比如服务端请求鉴权 ↓ 限流 ↓ 日志 ↓ 灰度 ↓ 业务执行很多 Web 框架、中间件、本质上都用了类似责任链的思想。4. 诊断服务处理比如格式校验 ↓ 会话校验 ↓ 安全校验 ↓ 参数校验 ↓ 服务执行 ↓ 响应生成诊断请求天然适合按链路处理。5. OTA 前置检查比如升级前检查电量检查 ↓ 车速检查 ↓ 档位检查 ↓ 网络检查 ↓ 存储空间检查 ↓ 版本兼容检查每个检查器可以独立开发、独立测试。6. 数据处理流水线比如采集 ↓ 清洗 ↓ 脱敏 ↓ 压缩 ↓ 缓存 ↓ 上传如果每一步可以独立组合责任链也很合适。十六、责任链不适合哪些场景责任链不是万能的。1. 只有一两个简单判断如果只是if(!valid)return;DoSomething();没必要上责任链。直接写更清楚。2. 流程强固定而且变化很少如果流程永远固定步骤也很稳定模板方法可能更合适。责任链的价值在于可组合、可扩展、可中断。如果这些都用不上就没必要复杂化。3. 节点之间强依赖如果后一个节点强依赖前一个节点的内部实现细节责任链会变得很脆弱。责任链里的节点最好是职责独立 输入输出清晰 只通过上下文协作不要让节点之间互相了解太多。4. 链路顺序难以管理如果系统里到处都在拼责任链而且每条链顺序都不一样就会产生新的复杂度。这时要考虑把链路组装集中起来。比如ChainFactory ChainBuilder 配置中心 流程编排器否则责任链会从“解耦工具”变成“排查噩梦”。十七、使用责任链时的几个注意点1. 链路顺序要明确责任链最大的风险之一就是顺序不清楚。比如先风控还是先权限 先幂等还是先执行 先记录日志还是先校验参数这些都要明确。最好不要让链路顺序散落在各个业务函数里。2. Handler 职责要小一个 Handler 最好只做一件事。不要写成PermissionAndVehicleAndRiskHandler这种名字一看就知道职责已经混了。责任链的价值正是让每一级处理器职责清楚。3. Context 不要变成垃圾桶责任链通常会传一个上下文对象。但上下文很容易膨胀。最后变成什么字段都往里面塞 谁都能改 谁都不知道哪个字段什么时候有效这会让链路变得难维护。所以 Context 要尽量保持清晰。能只读就只读。需要写入的字段要有明确语义。4. 中断原因要可观察责任链经常中途失败。所以失败原因一定要清楚。比如不要只返回failed而要返回vehicle offline risk blocked no permission battery too low否则线上排查会非常痛苦。5. 日志要能看到链路经过了哪些节点责任链一长排查问题时最怕不知道请求走到了哪一步。建议在关键节点记录进入哪个 Handler 是否通过 失败原因是什么 耗时多少尤其是远控、OTA、诊断、数据上报这类链路。6. 注意线程安全Handler 如果是无状态的可以多个请求复用。如果 Handler 内部有状态比如缓存、计数器、临时结果就要特别注意线程安全。更推荐Handler 尽量无状态 请求状态放在 Context 里这样复用起来更安全。十八、责任链的优缺点优点第一拆分多级处理逻辑。每个 Handler 只负责一个明确职责。第二新增节点更方便。新增校验、过滤、拦截逻辑时可以新增 Handler 再插入链路。第三支持中途拦截。任何一级不通过都可以直接终止后续处理。第四链路可以灵活组合。不同业务可以复用同一批 Handler但组合出不同链路。第五便于测试。每个 Handler 可以单独写单元测试。缺点第一链路过长时不容易追踪。请求经过很多节点后排查问题需要良好的日志和链路观测。第二顺序错误可能带来隐蔽问题。比如先执行后校验就很危险。第三Context 容易膨胀。如果所有节点都往上下文里塞字段会让上下文变得越来越乱。第四过度设计会增加复杂度。简单判断没必要拆成责任链。第五链路组装需要管理。如果链路在很多地方手写维护成本会升高。十九、使用责任链前先问这 6 个问题1. 这个请求是否需要经过多个处理步骤如果只有一个处理步骤就不需要责任链。2. 每个处理步骤是否相对独立如果每一步都有清晰职责责任链比较合适。3. 某些步骤是否会中断后续流程比如校验失败、风控拦截、状态不允许。如果需要中断责任链很适合。4. 处理步骤是否会增减或调整顺序如果链路经常变化责任链比写死在大函数里更灵活。5. 这些处理步骤是否需要复用如果多个业务都需要类似校验拆成 Handler 更有价值。6. 链路是否能被清楚管理和观测如果不能清楚知道请求经过了哪些节点责任链反而会增加排查难度。二十、总结责任链模式解决的是当一个请求需要经过多个处理节点并且每个节点都有机会处理、放行或拦截时如何把这些处理逻辑优雅地串起来。它不是为了把简单if-else强行拆成很多类。它真正想解决的是多级校验全部堆在一个函数里多级处理顺序不清楚新增处理步骤必须修改核心逻辑相同校验逻辑在多个业务里复制某一级失败后需要中断后续流程不同业务想复用同一批处理节点链路需要按车型、业务、配置灵活组合一句话概括责任链模式的重点不是“把代码拆成一串类”而是“让每个节点只负责自己那一关请求按链路逐级传递”。在车企软件里它很适合这些场景远程车控指令校验和执行诊断请求处理链OTA 升级前置检查告警过滤和上报数据采集、清洗、压缩、上传请求鉴权、限流、灰度、审计自动化测试执行前后的检查链车辆控制任务的多级拦截但它也不适合所有场景。如果流程骨架固定、只是部分步骤变化模板方法可能更合适。如果只是选择一种算法策略模式可能更合适。如果请求需要排队、撤销、重试命令模式可能更合适。如果只是给对象动态增强能力装饰器可能更合适。设计模式真正有价值的地方不是看到多个步骤就马上套责任链而是你能不能识别出“这些步骤是否应该独立成节点并通过链路组合起来”。如果上一篇模板方法提醒我们不要把“明明流程固定、只是步骤不同的业务”复制粘贴成一堆相似但不一致的代码。那么这一篇责任链模式提醒我们不要把“明明可以逐级校验、逐级处理、逐级拦截的请求”全部硬塞进一个越来越长的大函数里。如果这篇对你有帮助欢迎点赞、转发、关注。我们下一篇继续拆设计模式。