责任链模式实战:审批流里“层层把关“的艺术

一句话定位:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系------将这些对象连成一条链,沿着链传递请求,直到有一个对象处理它。

一、实战场景

公司内部系统里,报销审批是最典型的"链条式"业务。假设规则是:

  • 500 元以内:直属主管审批即可;
  • 500 ~ 5000 元:主管审批后,还需部门经理审批;
  • 5000 ~ 50000 元:主管 → 部门经理 → 财务总监;
  • 超过 50000 元:主管 → 部门经理 → 财务总监 → CEO。

如果把这些规则写成 if/else:

csharp 复制代码
public ApprovalResult Approve(ExpenseClaim claim)
{
    if (claim.Amount <= 500)
    {
        _supervisor.Approve(claim);
    }
    else if (claim.Amount <= 5000)
    {
        _supervisor.Approve(claim);
        _manager.Approve(claim);
    }
    else if (claim.Amount <= 50000)
    {
        _supervisor.Approve(claim);
        _manager.Approve(claim);
        _finance.Approve(claim);
    }
    else
    {
        _supervisor.Approve(claim);
        _manager.Approve(claim);
        _finance.Approve(claim);
        _ceo.Approve(claim);
    }
    return ...;
}

问题很明显:

  1. 审批规则的顺序和层级写死在业务代码里,规则一变(比如 5000 改成 8000)就要改这段逻辑;
  2. 审批人之间没有任何"传递"关系,每个层级都要业务层手动调用;
  3. 新加一个审批环节(比如"法务审批")要改这里,还要在 if/else 里再加分支。

责任链模式的解法:把每个审批人设计成"链上的一个节点",每个节点处理不了(或处理完自己那部分)就把请求传给下一个节点。业务层只需要把请求丢进链头,剩下的事由链自己完成。

二、设计思路

复制代码
          ExpenseClaim(请求:报销单)
                │
                ▼
┌─────────┐  通过/无权  ┌─────────┐  通过/无权  ┌─────────┐
│ 主管节点  │──────────▶│ 部门经理  │──────────▶│ 财务总监  │──▶...──▶│ CEO │
└─────────┘            └─────────┘            └─────────┘        └─────┘
  金额 ≤500 直接批        500~5000 批完即止        5000~50000

每个节点做两件事:

  1. 判断自己能不能处理(金额是否在权限内);
  2. 能处理就处理并结束;不能处理就交给下一个节点(也可以"处理完一部分再传递",取决于业务)。

三、代码实现(.NET 10 / C# 14)

3.1 请求模型

csharp 复制代码
namespace Approvals.Expenses;

public sealed record ExpenseClaim(
    string ClaimId,
    string EmployeeName,
    decimal Amount,
    string Reason);

public enum ApprovalDecision
{
    Approved,
    Rejected
}

public sealed record ApprovalResult(ApprovalDecision Decision, string ApprovedBy, string Remark);

3.2 抽象处理器:链上节点

csharp 复制代码
namespace Approvals.Expenses;

public abstract class ExpenseApprover
{
    private ExpenseApprover? _next;

    /// <summary>设置下一节点,返回下一节点便于链式构建</summary>
    public ExpenseApprover SetNext(ExpenseApprover next)
    {
        _next = next;
        return next;
    }

    protected ExpenseApprover? Next => _next;

    /// <summary>模板方法:先试自己,处理不了就传下去</summary>
    public async Task<ApprovalResult> HandleAsync(ExpenseClaim claim, CancellationToken ct = default)
    {
        var result = await TryApproveAsync(claim, ct);

        // 自己处理不了(null 表示无权处理),沿链传递
        if (result is null && _next is not null)
            return await _next.HandleAsync(claim, ct);

        return result ?? new ApprovalResult(ApprovalDecision.Rejected, "System", "无审批人可处理");
    }

    /// <summary>子类实现:能处理返回结果,不能处理返回 null</summary>
    protected abstract Task<ApprovalResult?> TryApproveAsync(ExpenseClaim claim, CancellationToken ct);
}

3.3 具体节点:四级审批人

csharp 复制代码
namespace Approvals.Expenses;

// 节点1:直属主管(500 元以内)
public sealed class SupervisorApprover(string name) : ExpenseApprover
{
    protected override Task<ApprovalResult?> TryApproveAsync(ExpenseClaim claim, CancellationToken ct)
        => claim.Amount <= 500
            ? Task.FromResult<ApprovalResult?>(new(ApprovalDecision.Approved, name, "主管审批通过"))
            : Task.FromResult<ApprovalResult?>(null);
}

// 节点2:部门经理(500 ~ 5000 元)
public sealed class DepartmentManagerApprover(string name) : ExpenseApprover
{
    protected override Task<ApprovalResult?> TryApproveAsync(ExpenseClaim claim, CancellationToken ct)
        => claim.Amount is > 500 and <= 5000
            ? Task.FromResult<ApprovalResult?>(new(ApprovalDecision.Approved, name, "部门经理审批通过"))
            : Task.FromResult<ApprovalResult?>(null);
}

// 节点3:财务总监(5000 ~ 50000 元)
public sealed class FinanceDirectorApprover(string name) : ExpenseApprover
{
    protected override Task<ApprovalResult?> TryApproveAsync(ExpenseClaim claim, CancellationToken ct)
        => claim.Amount is > 5000 and <= 50000
            ? Task.FromResult<ApprovalResult?>(new(ApprovalDecision.Approved, name, "财务总监审批通过"))
            : Task.FromResult<ApprovalResult?>(null);
}

// 节点4:CEO(50000 元以上)
public sealed class CeoApprover(string name) : ExpenseApprover
{
    protected override Task<ApprovalResult?> TryApproveAsync(ExpenseClaim claim, CancellationToken ct)
        => claim.Amount > 50000
            ? Task.FromResult<ApprovalResult?>(new(ApprovalDecision.Approved, name, "CEO 审批通过"))
            : Task.FromResult<ApprovalResult?>(null);
}

3.4 构建审批链

csharp 复制代码
namespace Approvals.Expenses;

public static class ApprovalChainFactory
{
    /// <summary>按公司当前规则组装审批链(规则变更只需改这里)</summary>
    public static ExpenseApprover BuildDefaultChain()
    {
        var supervisor = new SupervisorApprover("张主管");
        var manager = new DepartmentManagerApprover("李经理");
        var finance = new FinanceDirectorApprover("王总监");
        var ceo = new CeoApprover("刘总");

        // 链式连接:主管 → 经理 → 总监 → CEO
        supervisor.SetNext(manager).SetNext(finance).SetNext(ceo);

        return supervisor;   // 返回链头
    }
}

3.5 使用:请求丢进链头即可

csharp 复制代码
// Program.cs(.NET 10 顶层语句)
using Approvals.Expenses;

var chain = ApprovalChainFactory.BuildDefaultChain();

var claims = new[]
{
    new ExpenseClaim("CL-001", "张三", 300m, "打车费"),
    new ExpenseClaim("CL-002", "李四", 3000m, "出差机票"),
    new ExpenseClaim("CL-003", "王五", 30000m, "设备采购"),
    new ExpenseClaim("CL-004", "赵六", 100000m, "服务器集群")
};

foreach (var claim in claims)
{
    var result = await chain.HandleAsync(claim);
    Console.WriteLine($"{claim.ClaimId}(¥{claim.Amount})→ {result.ApprovedBy}:{result.Remark}");
}

// 输出示例:
// CL-001(¥300)→ 张主管:主管审批通过
// CL-002(¥3000)→ 李经理:部门经理审批通过
// CL-003(¥30000)→ 王总监:财务总监审批通过
// CL-004(¥100000)→ 刘总:CEO 审批通过

3.6 拓展:规则变了怎么办?

公司调整规则:"5000 以上且超过 20000 的需要法务会签"。

csharp 复制代码
// 新增一个节点,插进链中即可,业务代码零改动
public sealed class LegalApprover(string name) : ExpenseApprover
{
    protected override Task<ApprovalResult?> TryApproveAsync(ExpenseClaim claim, CancellationToken ct)
        => claim.Amount is > 20000
            ? Task.FromResult<ApprovalResult?>(new(ApprovalDecision.Approved, name, "法务会签通过"))
            : Task.FromResult<ApprovalResult?>(null);
}

// 组装时插入:主管 → 经理 → 财务 → 法务 → CEO

四、应用要点与注意事项

  1. 责任链的两种形态 :
    • 终止型:找到一个能处理的节点就结束(本示例------审批流);
    • 全链型:所有节点都处理一遍(中间件管道------每个请求都过日志、鉴权、限流)。ASP.NET Core 中间件就是全链型责任链的经典实现。
  2. 请求与处理解耦:发送方只需要知道链头,不知道也不关心谁最终处理。新增节点、调整顺序都不影响发送方。
  3. 注意链的健壮性:如果整条链都没有节点能处理,必须有兜底逻辑(示例中返回了"无审批人可处理")。链断了(某节点没设 Next)也要能优雅结束。
  4. 结合模板方法使用 :HandleAsync 的"先试自己、不行再传"骨架就是模板方法模式,子类只实现 TryApproveAsync。两个模式天然搭配。
  5. 与 if/else 的取舍:如果规则简单固定(两三个分支),直接写条件判断反而更直观;当节点数量、顺序、规则经常变化,或节点本身有复杂逻辑时,责任链才真正值得。

五、小结

责任链模式把"谁能处理"的判断从业务代码里解放出来,变成一条可动态组装、可插拔的链。审批流、异常处理、日志管道、请求过滤都是它的主场。它和模板方法、装饰器、中间件思想一脉相承------理解"链",就理解了现代框架中一大半的管道设计。

相关推荐
知守观2 小时前
Maven SNAPSHOT 更新排查:-U 无效、clean 无用,问题在 _remote.repositories(附完整链路)
java·后端·maven
IT_陈寒2 小时前
一文搞懂SpringBoot的核心概念
前端·人工智能·后端
Mikko73 小时前
fastjson 1.2.84 能在 JDK 7 上跑吗?实测:编译能过,一启动就 UnsupportedClassVersionError
java·后端·jdk·fastjson·安全漏洞
高频因子挖掘机3 小时前
股票 API 返回 HTTP 429 怎么办?Python 量化系统的限流排查与重试设计
后端·github·api
mldong3 小时前
为了 token 自由,我把 jeeflow 工作流引擎用 MoonBit 写了一遍
后端·架构
高频因子挖掘机4 小时前
从回测到实盘:量化策略为什么不应该直接依赖行情 API
后端·github·api
大鸡腿同学12 小时前
手心向上,手心向下
后端
CCblandy12 小时前
令牌桶在 0.9 倍负载超发 19%、固定窗口边界放行 20 个:四种限流算法同构实测
后端·python·算法·性能优化·架构