职责链模式是为审批/校验等多层易变流程提供可配置、可跳过、可记录的松耦合结构;其核心在于统一接口契约、动态组装、上下文透传与错误聚合,而非硬编码调用链。

直接上结论:职责链模式不是用来“串逻辑”的,而是用来把审批/校验这类多层、易变、需短路或透传的流程,变成可配置、可跳过、可记录的松耦合结构。硬写 if-else 链或手动 new HandlerA()->handle()->next()->handle(),不出三个月就会被新增节点、加签规则、临时绕过需求拖垮。
为什么不能用 new Handler() + 手动调用串起审批流
看似可控,实则埋下三个硬伤:
-
Request或上下文对象在各Handler间靠参数传递?一旦某个 Handler 忘记 return $context,后续全崩;靠静态变量或全局状态?并发下数据错乱是常态 - 异常堆栈里只看到
FinanceHandler::handle(),但根本不知道前一个DepartmentHeadHandler是否改写了$context['passed'],还是静默吞掉了错误 - 要支持“驳回后重提自动跳过已审节点”,就得在每个
handle()里查数据库状态、判断是否跳过——逻辑散落,维护成本指数级上升
ThinkPHP 中责任链必须满足的四个接口契约
不定义统一契约,链就只是伪链。每个 Handler 必须实现:
-
canHandle(array $context): bool—— 决定是否参与本次流转(比如金额超限才触发财务审批) -
handle(array $context): array—— 输入输出必须是同一结构,含['passed' => true, 'next' => true, 'errors' => []]等字段 -
getOrder(): int—— 用于排序,避免依赖配置数组顺序(防止 config/approval_chain.php 被误调换) -
getName(): string—— 日志和监控需要可读标识,别用get_class($this),类名可能带命名空间
示例:FinanceHandler::canHandle() 可能检查 $context['amount'] > 10000 且 !isset($context['finance_sign']),不满足就直接返回 false,链自动跳过该节点。
审批链初始化必须脱离 Controller,且支持运行时动态组装
Controller 只负责接收请求、构造初始 $context、调用 Service 并返回响应。链的组装必须在 Service 层完成:
- 从
config/approval_chain.php读取 handler 类名列表,用Loader::import()或容器make()实例化,别用new硬编码 - 按
getOrder()排序后,用循环调用$handlers[$i]->setNext($handlers[$i+1] ?? null)构建引用链 - 若某节点需“加签”,不在链上新增 Handler,而是在其
handle()内部查Cache::get('finance_handler_strategy'),值为recheck_on_reject时主动调用上一节点的handle()(注意防死循环)
这样新增合规部审批?只需在配置里加个类名,再写一个实现四接口的 ComplianceHandler,零侵入主流程。
最容易被忽略的点:错误聚合与状态透传必须由链本身保障
每个 Handler 不该抛 Exception 中断链,也不该直接 return false。真正的关键在于:$context 必须携带可累积的 errors 和明确的 passed 标志。否则前端收到的永远是“审批失败”,却不知是部门负责人没签,还是财务系统超时,还是金额格式错误。链末端的 Service 才负责汇总所有 $context['errors'],并决定返回 400 还是 200 + 提示列表。这点不做死,后续加审计日志、流程回溯、BI 统计全得返工。










