大家好,我是十三!欢迎来到十三Tech。
前阵子接入支付宝老版 SDK,他们的方法签名是 TradePay(amount int, currency, notifyURL string, extras map[string]string)——五个参数、四种类型,跟我们项目里 Payment.Pay(amount int) error 这个标准接口完全对不上。SDK 是外部维护的,不能改;项目里三十多个调用点都依赖 Payment 接口,更不能改。
最后花了三小时写了一个 AlipayAdapter——实现 Payment 接口,内部调 TradePay 并把参数填好。一个 20 行的文件,把"双方都不能动但必须协作"的问题解决了。
很多人把适配器讲成"让不兼容接口协作",但这个定义模糊到可以把任何 wrapper 都叫适配器。这一篇就聊聊适配器真正解决的场景是什么,以及它跟门面、代理的边界在哪儿。
一、适配器在解决什么——不是"加一层"
教科书定义:将一个类的接口转换成客户端期望的另一个接口。这话没错,但隐藏了一个前提约束——双方都不能改。
很多人误把"加一层包装"就叫适配器。但如果只是把 &Foo{a:1} 包装成 NewFoo(a int) *Foo,这不算适配器——这是语法糖。真正的适配器,是双方都因为合理理由不能改时,在中间架一座桥。
SDK 是外部维护的,改它意味着升级时要重新 patch;遗留系统的接口被几十个调用点依赖,改它意味着大范围回归测试。这两种场景的共同点——修改成本远高于加一层包装的成本。适配器就是为这种场景生的。
判断口诀:双方都不能改 + 接口形状不匹配 → 适配器;只是想加功能 → 装饰器;只是想统一入口 → 门面。
二、对象适配器在 Go 里怎么写
适配器分两种:类适配器(继承,Go 不支持)和对象适配器(组合,Go 推荐)。Go 工程里只有对象适配器一种写法。
type Payment interface {
Pay(amount int) error
}
type AlipaySDK struct{}
func (a *AlipaySDK) TradePay(amount int, currency, notifyURL string) error { return nil }
type AlipayAdapter struct {
sdk *AlipaySDK
currency string
notifyURL string
}
func (a *AlipayAdapter) Pay(amount int) error {
return a.sdk.TradePay(amount, a.currency, a.notifyURL)
}
这段代码的关键是 AlipayAdapter 既实现了项目的 Payment 接口,又内部持有 AlipaySDK 的引用。客户端看到的是 Payment,背后是 AlipaySDK.TradePay。适配器只做"参数翻译"和"方法转发"——它不做业务逻辑。
写适配器时一个容易踩的坑:错误类型也要适配。SDK 返回 *AlipayError,项目规范要求 error,适配器要做一次错误包装(fmt.Errorf("alipay: %w", err))——否则错误类型漏出去,调用方又得做类型断言,适配层的价值就废了。
三、适配器、门面、代理——三个"加一层"的边界
结构型模式里有三个孪生兄弟容易混——适配器、门面、代理。它们都是"加一层",但意图完全不同。
适配器:双方接口不一样但必须配合。意图是翻译。SDK 集成、遗留系统兼容、多版本抽象层都是适配器。 门面:客户端被多个子系统折磨。意图是简化入口。门面不翻译接口,是把多个调用合并成一个。 代理:接口一样但想加控制。意图是拦截。缓存、权限、懒加载、RPC 都是代理。
判断口诀:接口不一样 → 适配器;接口多个合并 → 门面;接口一样加控制 → 代理。
适配器的反模式主要有两个。
反模式一:用适配器替代直接调用。如果 SDK 的接口本来就能直接用,硬加一层适配器徒增样板代码——增加间接层、增加测试 mock、增加维护负担。适配器只在"接口真的对不上"时才有价值。
反模式二:适配器里塞业务逻辑。适配器应该薄——只做参数翻译、方法转发、错误包装。如果适配器里写了"金额校验""风控检查""日志埋点",它已经退化成了业务层,该拆出去。
Go 标准库里适配器最典型的地方是 sort.Interface——任何自定义类型只要实现 Len/Less/Swap 三个方法,就能被 sort.Sort 处理。sort.Sort 不知道也不关心你传的是切片、链表还是自定义容器。这就是适配器思想的精髓——算法面向抽象接口,类型适配进抽象。
回头看那个支付 SDK——值得用适配器吗?值得,SDK 不能改、项目接口不能改、两者形状真的不匹配,正是适配器的甜区。但如果只是把 NewFoo(a) 包装成 NewFooWithDefault() 这种小事,直接写两个构造函数就行。
适配器真正教会我的,不是"翻译接口",而是修改成本决定设计选择——双方都不能改时,加一层比硬改任何一边都便宜。这个权衡比模式本身重要得多。
下一篇讲装饰器,它和适配器很像(都是加一层),但装饰器不翻译接口,而是在同接口上动态叠加功能。
关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。
联系方式:569893882@qq.com GitHub:@TriTechAI
