Skip to content

适配器与外观模式

适配器(Adapter)和外观(Facade)都是结构型模式,它们都不改变现有系统,而是通过「包装」提供新的接口。区别在于:适配器把一个接口转换成另一个接口(让不兼容的能协作);外观为复杂子系统提供一个简化的统一入口。本章我们看 Go 如何用接口和嵌入轻量地实现这两个模式。

一、适配器模式

1. 意图

适配器模式:将一个类的接口转换成客户端期望的另一个接口。适配器让原本接口不兼容的类可以一起工作。

经典比喻:你的笔记本只有 USB-C 接口,但鼠标是 USB-A 的,你需要一个「转接头」。这个转接头就是适配器。

2. Go 实现:实现目标接口,包装被适配者

Go 的适配器写法非常直接:定义一个适配器结构体,让它实现目标接口,内部持有一个被适配者。

go
package main

import "fmt"

// === 目标接口:客户端期望的接口 ===
type Logger interface {
	Log(level, msg string)
}

// === 被适配者:第三方日志库,接口不兼容 ===
type LegacyLogger struct{}

// 它的方法签名跟 Logger 不一样
func (l *LegacyLogger) WriteMessage(category, message string) {
	fmt.Printf("[Legacy][%s] %s\n", category, message)
}

// === 适配器:把 LegacyLogger 包装成 Logger ===
type LegacyLoggerAdapter struct {
	legacy *LegacyLogger
}

func NewLegacyLoggerAdapter(l *LegacyLogger) *LegacyLoggerAdapter {
	return &LegacyLoggerAdapter{legacy: l}
}

// 实现 Logger 接口,内部转发给被适配者
func (a *LegacyLoggerAdapter) Log(level, msg string) {
	// 适配转换:level -> category
	a.legacy.WriteMessage(level, msg)
}

// 客户端只依赖 Logger 接口
func doWork(l Logger) {
	l.Log("INFO", "服务启动")
	l.Log("ERROR", "数据库连接失败")
}

func main() {
	legacy := &LegacyLogger{}
	adapter := NewLegacyLoggerAdapter(legacy)
	doWork(adapter)
}

关键点:

  • Logger 是目标接口(客户端想要的样子)。
  • LegacyLogger 是被适配者(已有的、不兼容的)。
  • LegacyLoggerAdapter 实现 Logger,内部持有 LegacyLogger,把 Log 调用转成 WriteMessage
  • 客户端 doWork 完全感知不到适配器的存在,它只跟 Logger 打交道。

3. 类适配器 vs 对象适配器(Go 只有对象适配器)

GoF 原书把适配器分为两种:

  • 类适配器:同时继承「目标接口」和「被适配者」,利用多重继承在适配时覆盖方法。需要语言支持多重继承(如 C++)或同时 extends + implements(如 Java)。
  • 对象适配器:通过组合(持有被适配者的引用)实现适配。

Go 没有继承,所以 Go 只有对象适配器。这反而是好事——对象适配器比类适配器更灵活,能适配被适配者的子类型,且不会破坏封装。

4. 实战示例:适配旧版日志库到新接口

下面模拟一个更真实的场景:老系统用 fmt.Printf 风格的日志,新系统统一用结构化的 Logger 接口,我们用适配器平滑迁移。

go
package main

import (
	"fmt"
	"strings"
)

// 新系统统一接口
type StructuredLogger interface {
	Info(msg string, fields map[string]any)
	Error(msg string, fields map[string]any)
}

// 老系统的日志器:只能输出文本
type OldPrinter struct{}

func (OldPrinter) PrintLine(prefix, text string) {
	fmt.Printf("[%s] %s\n", prefix, text)
}

// 适配器:把 OldPrinter 适配成 StructuredLogger
type PrinterAdapter struct {
	printer OldPrinter
}

func NewPrinterAdapter(p OldPrinter) *PrinterAdapter {
	return &PrinterAdapter{printer: p}
}

func (a *PrinterAdapter) Info(msg string, fields map[string]any) {
	a.printer.PrintLine("INFO", formatLine(msg, fields))
}

func (a *PrinterAdapter) Error(msg string, fields map[string]any) {
	a.printer.PrintLine("ERROR", formatLine(msg, fields))
}

func formatLine(msg string, fields map[string]any) string {
	parts := []string{msg}
	for k, v := range fields {
		parts = append(parts, fmt.Sprintf("%s=%v", k, v))
	}
	return strings.Join(parts, " ")
}

// 业务代码用新接口
func processOrder(id string, logger StructuredLogger) {
	logger.Info("订单处理开始", map[string]any{"order_id": id})
	logger.Error("库存不足", map[string]any{"order_id": id, "stock": 0})
}

func main() {
	// 老打印机通过适配器接入新系统
	old := OldPrinter{}
	adapter := NewPrinterAdapter(old)
	processOrder("ORD-001", adapter)
}

这种「老接口 + 适配器 + 新接口」的迁移方式在重构中极其常用:你不必一次性重写所有代码,只要给老组件套个适配器,就能让它在新的接口体系下工作。

二、外观模式

1. 意图

外观模式:为子系统中的一组接口提供一个一致的、简化的界面。外观让子系统更容易使用,客户端不需要了解子系统内部的复杂结构。

经典比喻:你点外卖,只需要点几下手机,背后涉及订单系统、支付系统、商家接单、骑手派单、结算等一大堆子系统。外卖 App 就是这一大堆子系统的「外观」。

2. Go 实现:提供一个简化的 API 层

外观模式在 Go 里就是一个普通的结构体/函数,把多个子系统的调用聚合起来:

go
package main

import "fmt"

// === 子系统 1:库存 ===
type InventoryService struct{}

func (InventoryService) Check(itemID string, qty int) bool {
	fmt.Printf("检查库存: %s x%d\n", itemID, qty)
	return true
}
func (InventoryService) Deduct(itemID string, qty int) {
	fmt.Printf("扣减库存: %s x%d\n", itemID, qty)
}

// === 子系统 2:支付 ===
type PaymentService struct{}

func (PaymentService) Charge(amount float64) bool {
	fmt.Printf("扣款: %.2f\n", amount)
	return true
}

// === 子系统 3:物流 ===
type ShippingService struct{}

func (ShippingService) Ship(orderID, address string) {
	fmt.Printf("发货: 订单 %s -> %s\n", orderID, address)
}

// === 子系统 4:通知 ===
type NotificationService struct{}

func (NotificationService) Send(userID, msg string) {
	fmt.Printf("通知 %s: %s\n", userID, msg)
}

// === 外观:把上述子系统聚合成一个简单的下单接口 ===
type OrderFacade struct {
	inventory  InventoryService
	payment    PaymentService
	shipping   ShippingService
	notify     NotificationService
}

func NewOrderFacade() *OrderFacade {
	return &OrderFacade{
		inventory: InventoryService{},
		payment:   PaymentService{},
		shipping:  ShippingService{},
		notify:    NotificationService{},
	}
}

// PlaceOrder 是外观提供的「简化 API」
func (f *OrderFacade) PlaceOrder(userID, itemID string, qty int, amount float64, address string) bool {
	fmt.Println("=== 开始下单 ===")
	if !f.inventory.Check(itemID, qty) {
		f.notify.Send(userID, "库存不足")
		return false
	}
	if !f.payment.Charge(amount) {
		f.notify.Send(userID, "支付失败")
		return false
	}
	f.inventory.Deduct(itemID, qty)
	orderID := "ORD-" + itemID
	f.shipping.Ship(orderID, address)
	f.notify.Send(userID, "下单成功,订单号 "+orderID)
	fmt.Println("=== 下单完成 ===")
	return true
}

func main() {
	facade := NewOrderFacade()
	facade.PlaceOrder("user-001", "SKU-100", 2, 199.0, "北京朝阳区")
}

外观模式的关键:

  • OrderFacade 内部持有 4 个子系统,对外只暴露一个 PlaceOrder 方法。
  • 客户端不需要知道库存、支付、物流、通知怎么协作,外观替它编排好。
  • 外观不增加新功能,只是「整合」。

3. 外观 vs 适配器的区别

两者都「包装」,但意图不同:

维度适配器外观
目标转换 一个 不兼容接口简化 一组 子系统接口
对象数量一对一(一个适配器包一个被适配者)一对多(一个外观包多个子系统)
动机让已有接口能用让复杂变简单
是否改变接口是(转成新接口)是(提供新的高层接口)

简单记忆:适配器是「翻译官」,外观是「前台」。

三、标准库中的应用

1. io 包的适配器

Go 标准库的 io 包是适配器模式的宝库。它定义了 io.Readerio.Writer 两个核心接口,并提供大量「把某种类型适配成 Reader/Writer」的工具:

  • strings.NewReader:把 string 适配成 io.Reader
  • bytes.NewReader:把 []byte 适配成 io.Reader
  • bufio.NewReader:把一个 io.Reader 包装成带缓冲的 Reader。
  • ioutil.NopCloser / io.NopCloser:给一个 Reader 加上 Close 方法,适配成 io.ReadCloser
go
package main

import (
	"bufio"
	"fmt"
	"io"
	"strings"
)

func main() {
	// 把 string 适配成 io.Reader
	r := strings.NewReader("hello\nworld\n")

	// 再用 bufio 包装成带缓冲的 Reader
	br := bufio.NewReader(r)
	for {
		line, err := br.ReadString('\n')
		if line != "" {
			fmt.Printf("读到: %s", line)
		}
		if err == io.EOF {
			break
		}
	}

	// io.NopCloser:把 Reader 适配成 ReadCloser
	rc := io.NopCloser(strings.NewReader("data"))
	defer rc.Close()
	fmt.Println("rc 类型已实现 io.ReadCloser")
}

io 包的设计完美体现了适配器思想:定义极简接口(Reader/Writer),然后提供一堆适配器让各种数据源/汇都满足这个接口。这是 Go 标准库最具影响力的设计之一。

2. sql.DB 对 driver 的适配

database/sqlsql.DB 并不直接连接数据库,它把具体的协议交互委托给 driver.Driverdriver.Conn。可以理解为:sql.DB 是面向用户的「外观」,而 driver 包是各数据库驱动的「适配目标接口」。

  • driver.Conn 适配具体的数据库连接。
  • sql.DB 在其之上提供连接池、预处理、事务等高层 API(外观)。

这种「外观 + 适配器」组合是 Go 标准库的常见架构:定义抽象接口(适配目标),用外观提供易用的高层 API。

3. net/http 的适配

http.ResponseWriterhttp.Request 也可以看作外观:HTTP 协议本身很复杂(状态行、头部、body、chunked 编码等),但 http 包把这些细节封装到两个对象里,开发者只跟它们打交道就能写 Web 服务。

四、综合实战:日志系统重构

下面这个例子综合使用适配器和外观:把一个老日志库适配进新系统,再用外观提供一个简洁的「业务日志」入口。

go
package main

import (
	"fmt"
	"strings"
	"time"
)

// === 老日志库(不可改) ===
type FileLogger struct {
	path string
}

func (f *FileLogger) AppendLine(line string) {
	fmt.Printf("[FILE:%s] %s\n", f.path, line)
}

// === 新系统接口 ===
type AppLogger interface {
	Info(msg string)
	Error(msg string)
}

// === 适配器:FileLogger -> AppLogger ===
type FileLoggerAdapter struct {
	inner *FileLogger
}

func NewFileLoggerAdapter(path string) *FileLoggerAdapter {
	return &FileLoggerAdapter{inner: &FileLogger{path: path}}
}

func (a *FileLoggerAdapter) Info(msg string) {
	a.inner.AppendLine(fmt.Sprintf("[INFO] %s", msg))
}

func (a *FileLoggerAdapter) Error(msg string) {
	a.inner.AppendLine(fmt.Sprintf("[ERROR] %s", msg))
}

// === 外观:业务日志,附加时间戳和上下文 ===
type BusinessLoggerFacade struct {
	logger  AppLogger
	context map[string]string
}

func NewBusinessLogger(logger AppLogger) *BusinessLoggerFacade {
	return &BusinessLoggerFacade{logger: logger, context: map[string]string{}}
}

func (b *BusinessLoggerFacade) WithContext(k, v string) *BusinessLoggerFacade {
	b.context[k] = v
	return b
}

func (b *BusinessLoggerFacade) Info(msg string) {
	b.logger.Info(b.decorate(msg))
}

func (b *BusinessLoggerFacade) Error(msg string) {
	b.logger.Error(b.decorate(msg))
}

func (b *BusinessLoggerFacade) decorate(msg string) string {
	parts := []string{time.Now().Format("15:04:05")}
	for k, v := range b.context {
		parts = append(parts, k+"="+v)
	}
	parts = append(parts, msg)
	return strings.Join(parts, " ")
}

// === 业务使用 ===
func checkout(userID string, logger *BusinessLoggerFacade) {
	logger.WithContext("user", userID).Info("开始结算")
	logger.Error("优惠券已过期")
}

func main() {
	// 老日志库通过适配器接入
	adapter := NewFileLoggerAdapter("app.log")
	// 外观进一步封装
	biz := NewBusinessLogger(adapter)
	checkout("user-123", biz)
}

这个例子体现了实际项目中的典型分层:

  • 底层是老 FileLogger(不可改)。
  • 适配器把它适配成新接口 AppLogger
  • 外观再封装一层,加上业务上下文(时间戳、用户 ID 等),让业务代码调用更简单。
  • 业务代码 checkout 只用一个 BusinessLoggerFacade,不关心底层是文件还是别的。

五、外观模式 vs 中介者模式

外观和中介者(Mediator,本系列不展开)都「集中管理交互」,但区别:

  • 外观是 单向 的:外观调用子系统,子系统不会回调外观。
  • 中介者是 双向 的:同事对象之间通过中介者互相通信。

外观简化外部对子系统的调用,中介者简化子系统内部对象之间的相互引用。如果子系统内部对象相互依赖混乱,那是中介者的活;如果只是对外接口太复杂,用外观。

六、使用外观模式的注意事项

  • 外观不封装所有功能:外观只提供「常用」的简化 API,复杂的细粒度操作仍应允许客户端直接访问子系统。不要让外观变成「唯一入口」,否则又回到了过度封装。
  • 外观可以有多个:一个子系统可以为不同使用方提供多个外观(如「管理后台外观」「客户端外观」)。
  • 外观不是「上帝对象」:不要把所有逻辑都塞进外观,否则外观本身会变成新的复杂源。外观只做编排,业务逻辑应在子系统内。
  • 外观与分层架构:Go 项目里,service 层、handler 层本质上都是外观——它们把底层多个 repository、client 的调用整合成对外接口。

七、小结

  • 适配器把一个不兼容接口转换成目标接口,让原本无法协作的对象一起工作;Go 只有对象适配器(靠组合 + 实现目标接口)。
  • 外观为复杂子系统提供一个简化的统一入口,降低使用成本;Go 里就是一个聚合了多个子系统的结构体。
  • 适配器是「一对一翻译」,外观是「一对多整合」,意图不同。
  • Go 标准库大量使用这两个模式:io 包的各类 Reader/Writer 适配器、sql.DB 对 driver 的适配和外观、http 包对 HTTP 协议的封装。
  • 重构中「老接口 + 适配器 + 新接口 + 外观」是平滑迁移的常见组合。
  • 外观不要变成上帝对象,它只负责编排,业务逻辑留给子系统。

下一篇讲装饰器与代理模式——它们结构相似(都是包装),但意图截然不同。