11-14.【错误处理】当模块被多个模块依赖时,Typed Error 如何保证向后兼容?

2 阅读3分钟

在多模块架构中,Typed Error 是一把双刃剑。它提供了极强的类型安全性,但由于它是“强契约”,一旦你向作为 Typed Error 的枚举中添加一个新成员(Case),所有依赖该模块的代码都会因为 switch 语句不再穷举(Non-exhaustive)而编译失败

要在保证向后兼容的同时利用 Typed Error,可以采用以下防御式设计策略:


1. 使用 @frozen 枚举(仅限 Library Evolution 模式)

如果你在构建一个开启了 Library Evolution 的二进制框架(如 XCFramework),Swift 要求明确区分枚举是否会改变。

  • @frozen:承诺永远不会增加、删除或更改 Case。编译器会进行极限优化。
  • 非冻结枚举(默认) :如果你不加 @frozen,依赖方在 switch 时必须处理 @unknown default

Swift

@frozen public enum AuthError: Error {
    case invalidPassword
    case userNotFound
    // 未来无法增加新 Case,否则破坏二进制兼容性
}

2. “预留槽位”与包装模式 (The Wrapper Pattern)

这是最通用的做法。不要将具体的业务细节全部定义为枚举成员,而是定义一个通用的错误分类,并利用关联值携带具体信息。

Swift

public enum ModuleError: Error {
    case serviceUnavailable
    case validationFailed(reason: String) // 使用 String 或结构体,而不是枚举
    case other(Error) // 预留捕获所有未知情况的槽位
}
  • 兼容性优势:当你增加一种新的验证失败逻辑时,只需改变 reason 字符串,而不需要改变 ModuleError 的定义,依赖方的 switch 逻辑不会被破坏。

3. 错误分层:从特定到通用

当一个模块被广泛依赖时,不要在 Public 接口中抛出最底层的特定错误,而是抛出该模块的抽象根错误

  • 内部层:使用极其详尽的 InternalDetailError

  • 公共层:将内部错误映射为 PublicModuleError

    • 即使内部增加了一百种报错原因,只要它们都归属于“网络异常”,公共层的枚举就无需变动。

4. 利用 Protocol 隐藏具体类型

这是最解耦的方案。虽然函数签名仍然使用 throws(any ModuleErrorProtocol),但通过协议定义行为。

Swift

public protocol NetworkErrorProtocol: Error {
    var isRetryable: Bool { get }
}

// 模块内部可以随时增加实现该协议的新结构体,而不会破坏 API
public func fetchData() throws(any NetworkErrorProtocol) { ... }

5. 结构体替代枚举 (Struct-based Errors)

枚举之所以容易破坏兼容性,是因为它的“穷举性”。如果改用结构体模拟错误,则具有极佳的扩展性。

Swift

public struct TransferError: Error, Equatable {
    public let code: Code
    public let message: String

    public struct Code: Equatable {
        public static let insufficientFunds = Code(rawValue: 1)
        public static let invalidAccount = Code(rawValue: 2)
        private let rawValue: Int
    }
}
  • 兼容性优势:增加一个新的 static let 常量不会导致调用方的代码编译失败(因为调用方通常使用 if error.code == .insufficientFunds 而不是 switch)。

总结:防御式兼容性选择表

场景推荐方案兼容性级别缺点
底层核心库@frozen 枚举极高(不可变)无法增加新错误
业务逻辑模块包装枚举 (关联值)失去部分类型细分优势
插件式架构基于 Protocol 的错误极高无法直接进行 switch 匹配
快速迭代模块结构体 (Static Constants)无法强制调用方处理所有情况