在模块化架构中设计 Typed Error 的最大挑战在于平衡:既要提供编译时的明确性,又要避免强耦合导致的修改困难(Fragility)。
如果不加克制地暴露底层的具体错误,任何内部逻辑的微调都可能导致所有依赖方编译失败。
1. 核心设计模式:抽象包装(The Wrapper Pattern)
不要直接抛出模块内部的原始错误,而是定义一个面向接口的 Error 枚举。这相当于在模块边界建立了一个“防腐层”。
设计策略:
- 隐藏实现细节:将具体的底层错误(如
Alamofire.AFError或CoreDataError)封装在模块内部,不暴露给依赖方。 - 分类暴露:只暴露调用方有能力处理或需要知道的错误。
Swift
// 模块 A 的 Public 边界
public enum NetworkModuleError: Error {
case connectionLost
case unauthorized
case serverSide(code: Int)
case internalInconsistency // 隐藏所有底层的解析错误、数据库崩溃等
}
public func fetchData() throws(NetworkModuleError) -> Data { ... }
2. 利用泛型错误(Generic Errors)保持灵活性
如果你希望模块更加通用,可以使用泛型来传递错误类型。这在处理插件或中间件时非常有用,且不会破坏边界。
Swift
public struct ModuleResult<E: Error> {
public func execute<T>(_ action: () throws(E) -> T) throws(E) -> T {
try action()
}
}
3. 设计“不透明”但“可分类”的错误
如果你不想暴露完整的枚举分支,可以利用 Swift 的 Protocol 来设计错误。依赖方只需要判断错误所属的“类别”,而不需要识别具体类型。
Swift
public protocol RetriableError: Error {
var shouldRetry: Bool { get }
}
// 依赖方只需要关心这个:
do {
try module.performAction()
} catch let error as RetriableError where error.shouldRetry {
// 自动重试逻辑,无需知道具体是哪个子模块崩了
} catch { ... }
4. 转换技巧:从 Internal 到 Public
在模块内部,你可以使用普通 throws 甚至更细粒度的 Typed Error。但在最终暴露给依赖方的函数中,使用 do-catch 进行统一映射。
Swift
func internalOperation() throws(InternalDBError) { ... }
// Public 接口
public func publicAPI() throws(PublicModuleError) {
do {
try internalOperation()
} catch .diskFull {
throw .storageIssue // 映射
} catch {
throw .unknown // 屏蔽无关紧要的细节
}
}
5. 什么时候该回避 Typed Error?
为了不破坏模块边界,在以下场景中,传统的 any Error 反而更优:
- 多插件系统:如果你的模块允许第三方注入代码,你无法预知他们会抛出什么错误。
- 高度动态的应用层:业务需求变化极快时,强类型的错误契约会成为重构的枷锁。
- 跨团队大型协作:如果每次增加一个 Error Case 都要让其他十个团队更新代码,那么
any Error的解耦价值就体现出来了。
总结:防御式边界设计表
| 需求 | 方案 | 边界影响 |
|---|---|---|
| 极致性能/安全性 | 具体的 Typed Error 枚举 | 强耦合:修改枚举需重新编译所有依赖 |
| 业务逻辑分发 | 抽象包装枚举 (Wrapper) | 中耦合:仅暴露业务相关的抽象分类 |
| 高度插件化/解耦 | any Error (传统 throws) | 零耦合:依赖方仅需处理 catch-all |
| 可复用逻辑 | Protocol-based Error | 松耦合:依赖方根据行为(Behavior)而非类型判断 |