Result 结构设计:成功/失败泛型、内部错误码、错误消息、堆栈上下文

模块边界与组件职责

Result 结构不是简单的代码工具,而是跨模块通信的边界契约。它强制将模块的出口统一为两种状态,消除隐式运行时错误。各组件职责如下:

组件 模块边界角色 核心职责
Result<T, E> 基类 通用合约 承载 IsSuccess 状态机,禁止直接实例化
Success<T> 正常出口 携带业务实体 T Value,触发调用方正常流程
Failure<E> 异常出口 携带 ErrorCode + ErrorMessage + StackTrace
ErrorCode 程序化错误标识 整型或枚举,按模块前缀隔离(MOD_A_1001)
DiagnosticContext 调试与追踪 记录调用链 ID、参数快照、时间戳,支持跨模块关联
架构约束:Result 应当成为模块对外暴露的唯一返回值类型。禁止在公共接口中直接抛出原生异常或返回裸实体,保证调用方无需猜测出口状态。

数据流与控制流:强制路由

调用方通过 Result 对象的显式状态检查,将控制流从“猜测”变为“强制匹配”。

  • 正向流:模块 A → 入口网关 → 模块 B 服务 → 返回 Success<T> → 模块 A 解包 Value 执行业务。
  • 保护流:模块 B 内部验证失败 → 返回 Failure<E> → 模块 A 检查 ErrorCode → 降级或重试。
  • 上下文传播流:DiagnosticContext 随 Failure 附加上报,不因跨模块而丢失堆栈。
跨模块 Result 控制流与状态路由 跨模块 Result 强制路由与控制流 模块 A 调用方 入口网关 模块 B 业务服务 Result<T, E> 对象 IsSuccess / ErrorCode / Context 解包 Value 解析 Error / 降级 请求 返回 Result IsSuccess = true IsSuccess = false Result 强制调用方编译时识别两种状态,消除隐式运行时错误。 DiagnosticContext 附加跟踪 ID 和参数快照,支撑跨模块可观测性。
数据流控制要点:Result 不允许跨模块裸传递异常信息。错误码必须是预定义的内部枚举,不允许携带模块 B 的内部堆栈字符串穿透到模块 A。若需要堆栈,必须通过 DiagnosticContext 字段附加,且不得影响序列化主体。

选型权衡

选型维度 强类型泛型方案 弱类型 / 动态方案 架构建议
错误确定性 高:编译器强制处理每种错误码 低:调用方易忽略错误码 模块间接口必须强类型
序列化友好度 中:需设计泛型序列化转换器 高:自由 JSON 对象 核心模块使用强类型,边缘使用灵活契约
版本演进成本 中:增加新错误码需修改枚举 低:字符串可随时新增 使用语义化错误码前缀,预留 Reserved 段
跨语言互操作 低:依赖具体语言泛型实现 高:与 Protobuf/Thrift 亲和 边界定义 IDL,内部实现强类型封装
风险警告:切勿在 ErrorCode 中使用魔数或链式异常码。错误码必须是一等公民,纳入模块 API 文档。堆栈上下文不要无条件捕获全部参数,防止敏感数据泄露。推荐启用字段级白名单机制,只记录预先声明的上下文字段。

落地步骤

  1. 定义泛型基类:在公共核心库中声明 Result<TResult, TError>,提供 Match 模式匹配方法。
  2. 实现工厂方法:创建 Result.Success(value) 与 Result.Failure(errorCode, message, context) 静态入口。
  3. 设计内部错误码枚举:按模块前缀隔离,例如 ModuleA_Error 包含 InvalidParameter=1001、DependencyFailed=1002。
  4. 建立堆栈上下文契约:定义 IDiagnosticContext 接口,支持附加 TraceId、SpanId、InputSnapshot。
  5. 序列化适配:为 Result 编写自定义 JSON 序列化器,将 Success/Failure 统一输出为 { "status": "...", "data": "...", "error": {...} }。
  6. 集成 CI 门禁:在模块边界层添加静态分析规则,禁止 throw Exception 穿越公共接口。
// 核心 Result 结构示意图(C# 风格伪码)
public abstract record Result<TValue, TError>
{
    public bool IsSuccess { get; init; }
    public bool IsFailure => !IsSuccess;

    public T Match<T>(Func<TValue, T> onSuccess, Func<TError, T> onFailure);
}

public sealed record Success<TValue, TError>(TValue Value)
    : Result<TValue, TError>
{
    public bool IsSuccess => true;
}

public sealed record Failure<TValue, TError>(TError Error, DiagnosticContext Context)
    : Result<TValue, TError>
{
    public bool IsSuccess => false;
}

// 内部错误码枚举规则
public enum OrderModuleErrorCode
{
    Success = 0,
    InvalidOrderId = 2001,
    InsufficientBalance = 2002,
    PaymentGatewayTimeout = 2003,
}

验收标准

  • 零裸异常穿越:所有模块公共方法返回类型必须是 Result<,>,CI 扫描到 throw 关键字在接口层出现即告警。
  • 错误码覆盖率 100%:failure 无默认值,每个 Failure 实例必须附带一个有意义的 ErrorCode。
  • 上下文可回溯:从入口到出口,TraceId 在 Failure 的 DiagnosticContext 中完整保留,支持跨模块日志关联。
  • 结构体不可变:Result 一旦创建,其 IsSuccess 与内部数据不可变更,保证并发安全。
  • 序列化对称性:将 Result 序列化为 JSON 再反序列化后,状态与数据不丢失,单元测试覆盖 Success 和 Failure 两种路由。