包装模式四兄弟,为什么这么容易混淆?
学习设计模式时,总会遇到一个让人卡住的地方。
Adapter、Facade、Proxy、Decorator。这四个模式很容易混在一起。
你可能会想:“不都是包装另一个对象吗?”确实,四者都会在内部持有另一个对象,并站在它前面,所以外观很相似。
因此,很多人虽然记住了名称,实际写代码时却仍然分不清。
先说重点。
四种模式都会包装对象,但包装的“目的”不同。
Adapter 改变接口,Facade 隐藏复杂性,Proxy 控制访问,Decorator 添加功能。
记住这一句话,其他内容就顺理成章了。本文会把这四兄弟彻底区分开,让你在代码中准确判断它们。
比较之后如果需要实际实现,可以继续阅读 使用 Swift Adapter 包装遗留 API 和 使用 Swift Facade 简化子系统。
四兄弟一览对比表
在展开说明之前,先看一张表。我以前会把这张表贴在桌前,混淆时就看一眼。
| 模式 | 目的 | 接口 | 包装对象 |
|---|---|---|---|
| Adapter | 让不兼容的接口适配起来 | 改变 | 通常 1 个 |
| Facade | 为多个复杂子系统提供简单入口 | 重新创建 | 多个 |
| Proxy | 控制、延迟或缓存对原对象的访问 | 相同 | 1 个 |
| Decorator | 为原对象添加功能 | 相同 | 1 个 |
最重要的判断标准是“接口是否保持不变”。
Proxy 和 Decorator 的接口与原对象相同,因此调用方不必知道对象已被包装。
相反,Adapter 会有意改变接口,而 Facade 会直接创建一个全新的入口。
Adapter 和 Decorator 有什么区别?
最容易混淆的就是这一对。放到代码中看,区别会非常明显。
Adapter 是让“不匹配的东西配合起来”的转换插头,就是把 220V 设备接到 110V 插座上的那种 Adapter。
// 现有库只有 legacyPrint(text:)
// 当我们的代码期待 draw()时
protocol Renderer { func draw() }
struct LegacyLabel { func legacyPrint(_ t: String) { print(t) } }
struct LabelAdapter: Renderer {
let legacy: LegacyLabel
func draw() { legacy.legacyPrint("转换后的调用") }
}
LabelAdapter(legacy: LegacyLabel()).draw()
// 输出:转换后的调用
关键在于接口从 legacyPrint 改成了 draw。
Decorator 保持接口不变,只叠加功能,就像给咖啡加一份浓缩。
protocol Coffee { func cost() -> Int }
struct Americano: Coffee { func cost() -> Int { 4000 } }
struct ShotDecorator: Coffee {
let base: Coffee
func cost() -> Int { base.cost() + 500 }
}
ShotDecorator(base: Americano()).cost()
// 输出: 4500
两者都持有 base,但 Adapter 会改名,Decorator 会扩展行为。它们的目的完全不同。
Proxy 和 Facade 呢?
Proxy 的接口与原对象完全相同,但会在中间进行控制。
以图片加载为例,它不会立即加载真实图片,而是等到需要时再加载。这就是延迟加载。
缓存、权限检查和日志记录也经常由 Proxy 负责。调用方以为自己在调用原对象,实际上调用的是代理人。
Facade 的性质有些不同。它不是包装一个对象,而是把多个对象隐藏在后面。
以家庭影院为例,可以把打开投影仪、打开音箱、调暗灯光和开始播放的流程,合并为一次 watchMovie() 调用。不必了解内部细节,按一个按钮就能完成。
总结来说,Proxy 是“同一扇门前的门卫”,Facade 是“替你打开多扇门的服务台”。
什么时候使用,什么时候避免?
模式是工具,应当根据场景使用。强行套用只会让代码变得更复杂。
| 场景 | 判断 |
|---|---|
| 外部库的接口不兼容 | Adapter |
| 想为多个复杂模块提供简单入口 | Facade |
| 需要访问控制、延迟加载或缓存 | Proxy |
| 想灵活地组合并附加功能 | Decorator |
当然,也有明确应该避免它们的情况。
- 明明没有包装的理由,却因为“想用一下模式”而加入,结果只会增加层级。
- Decorator 叠得太多,调试时会变成地狱,根本追踪不到值是在哪里改变的。
- Facade 如果承担太多职责,也会变成一个巨大的代码块。
我会问自己:“现在包装的目的属于上面四个中的哪一个?”如果答不上来,就移除这个模式。
面试时可以这样回答
Q. Proxy 和 Decorator 都用相同接口包装对象,它们有什么区别?
A. 目的不同。Proxy 用于控制、延迟或缓存对原对象的访问,而原对象本身的功能保持不变。Decorator 则在保留原对象接口的同时添加功能。可以这样理解:Proxy 决定“要不要调用”,Decorator 决定“调用之后还要做什么”。
Q. Adapter 和 Facade 的区别是什么?
A. Adapter 通常包装一个对象,使不兼容的接口适配起来;Facade 包装多个子系统,提供一个简单的新入口。Adapter 是“翻译”接口,Facade 是“概括”接口。
总结
区分这四兄弟,最终只需问一个问题:“为什么要包装?”是要改变、隐藏、控制,还是为接口添加功能?
只要问清这个问题,面对代码时就不会再混淆。请保存今天的对比表,需要时随时拿出来参考。

