进行 iOS 开发时,你可能想保留基础功能,只额外加入日志或缓存。但如果为此开始继承类,没过多久继承树就会变得一团糟。
给网络客户端逐个添加功能时,很容易最终创建出类似 CachingLoggingRetryingClient 这样的类名。
今天就来梳理解决这个问题的 Swift 装饰器模式:无需继承,像洋葱一样将功能层层包裹起来。
装饰器模式使用遵循同一协议的对象包裹原对象,在不改动原始代码的情况下逐层添加功能。
先说结论。
- 先通过 协议定义公共行为
- 装饰器遵循该协议,并在内部 持有另一个相同协议类型的值
- 完成自己的工作,例如日志或缓存,然后将其余工作 委托给内部对象
- 按需继续包裹,功能就会不断叠加
什么是装饰器模式?它和继承有什么不同?
继承表示“是一种”关系。子类会继承父类的全部内容。
问题在于组合。如果通过继承创建带日志、带缓存以及两者兼具的客户端,每种组合都会产生一个类。
装饰器表示“被包裹的对象”关系。
由于包裹对象和被包裹对象共享 同一个接口,从外部看起来无法区分。因此无论包裹多少层,调用方代码都无需改变。
简而言之,继承在编译时固定关系,而装饰器可以在运行时自由组合。
用代码实现 Swift 装饰器模式
我们来创建一个用于加载数据的 DataLoader。首先是所有实现都要遵循的协议。
protocol DataLoader {
func load(id: String) -> Data?
}
// 执行实际工作的基础实现
struct NetworkLoader: DataLoader {
func load(id: String) -> Data? {
print("向网络发起 \(id) 请求")
return Data("payload-\(id)".utf8)
}
}
到这里为止,只是普通的协议和实现。
现在进入核心部分。装饰器遵循 DataLoader,并在内部持有另一个 DataLoader。
// 记录日志的装饰器
struct LoggingLoader: DataLoader {
let wrapped: DataLoader // 被包裹的对象
func load(id: String) -> Data? {
print("[日志] load 开始: \(id)")
let result = wrapped.load(id: id) // 委托实际工作
print("[日志] load 完成: \(id)")
return result
}
}
LoggingLoader只负责自己的工作(记录日志),真正的数据加载则交给 wrapped。
借助这种结构,无需关心包裹的具体对象是什么,只要它是 DataLoader 即可。
逐层叠加功能
这次再创建一个缓存装饰器,实际体验一下多层包裹。
final class CachingLoader: DataLoader {
let wrapped: DataLoader
private var cache: [String: Data] = [:]
init(wrapping loader: DataLoader) { self.wrapped = loader }
func load(id: String) -> Data? {
if let hit = cache[id] { return hit } // 命中缓存时立即返回
let data = wrapped.load(id: id) // 未命中时委托处理
cache[id] = data
return data
}
}
组装部分正是装饰器模式的精髓。
let loader = LoggingLoader(
wrapped: CachingLoader(wrapping: NetworkLoader())
)
从内向外阅读即可。我们先用缓存包裹网络加载器,再用日志包裹它。
调用进入后,流程依次是:日志 → 检查缓存 →(未命中时)访问网络。
如果想调整顺序,只需改变包裹顺序。NetworkLoader和 CachingLoader 都无需修改一行代码。
不需要的功能,直接移除对应层即可。
继承和装饰器,应该在什么时候使用?
没有哪一个永远正确,下面按场景区分。
| 场景 | 推荐方案 |
|---|---|
| 明确的“是一种”关系 | 继承 |
| 自由组合、移除功能 | 装饰器 |
| 在运行时启用或停用功能 | 装饰器 |
| 无法修改原始代码时 | 装饰器 |
| 组合情况很多时 | 装饰器 |
对于日志、缓存、重试、添加认证请求头等 非核心的附加功能,装饰器尤其合适。
反过来,如果强行使用装饰器,包裹层数会增加,调试时调用栈也会变深。使用时应考虑这一点。
两个常见问题
问:应该使用 struct 还是 class?
如果装饰器需要直接持有状态,例如缓存,使用 class 更方便。单纯进行委托时,struct 就足够了。这就是上例只将缓存设为 class 的原因。
问:包裹层数变多会有性能问题吗?
由于调用会逐层向内传递,确实存在极其微小的开销。但与网络或磁盘 I/O 相比,这在大多数应用中都可以忽略。
装饰器模式归根结底就是定义一个协议,创建一个持有相同协议的对象,再通过它进行委托。
下次为了添加功能而翻找继承树时,不妨想想再包裹一层。代码会轻量得多。建议把今天的示例原样移到 playground 中试试看 🙂

