软件设计

Adapter、Facade、Proxy、Decorator 的区别

Adapter 用于转换接口,Facade 用于隐藏复杂性,Proxy 用于控制访问,Decorator 用于添加功能。本文从意图和选择标准出发,对比四种结构相似的包装模式。

5 分钟阅读
Adapter、Facade、Proxy、Decorator 的区别 封面图

包装模式四兄弟,为什么这么容易混淆?

学习设计模式时,总会遇到一个让人卡住的地方。

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 是“概括”接口。


总结

区分这四兄弟,最终只需问一个问题:“为什么要包装?”是要改变、隐藏、控制,还是为接口添加功能?

只要问清这个问题,面对代码时就不会再混淆。请保存今天的对比表,需要时随时拿出来参考。

延伸阅读