Go语言设计模式:适配器模式详解
文章目录
- 一、适配器模式概述
- 1.1 什么是适配器模式?
- 1.2 建造者模式的优缺点
- 1.3 适用场景
- 1.4 适配器模式的UML图与核心角色
- 二、 Go语言实现:对象适配器
- 第1步:定义目标接口
- 第2步:定义被适配者
- 第3步:创建适配器
- 第4步:客户端使用
- 对象适配器模式(完整版)
- 三、Go语言实现:类适配器
- 第1步和第2步:目标接口和被适配者(同上)
- 第3步:创建类适配器(通过嵌入)
- 第4步:客户端使用
- 类适配器模式(完整版)
- 四、Go语言实现:一个更实际的例子
- 第1步:定义目标接口
- 第2步:定义被适配者(第三方库)
- 第3步:创建适配器
- 第4步:客户端使用
- 实际应用示例:日志系统适配器(完整版)
一、适配器模式概述
1.1 什么是适配器模式?
适配器模式是一种结构型设计模式,它能使接口不兼容的对象能够相互合作。适配器模式就像一个中间人,它充当两个不同接口之间的桥梁,使得一个类的接口能够满足客户端的期望,而无需修改原始类的代码。现实生活中的比喻:
- 电源适配器/充电头:这是最经典的例子。你的笔记本电脑(客户端)需要一个三孔的Type-C接口(目标接口),但墙上只有两孔的插座(被适配者)。电源适配器(适配器)将两孔插座转换成了你电脑可以使用的三孔Type-C接口。
- 读卡器:你的电脑(客户端)有USB接口(目标接口),而你的SD卡(被适配者)无法直接插入。读卡器(适配器)作为桥梁,让电脑可以通过USB接口读取SD卡。
1.2 建造者模式的优缺点
优点
- 单一职责原则:你可以将接口转换代码从业务逻辑中分离出来,使代码结构更清晰。
- 开闭原则:你可以在不修改现有客户端代码的情况下,引入新的适配器来兼容新的接口。
- 解耦:客户端和被适配者之间没有直接耦合,它们都依赖于抽象(目标接口)。
缺点
- 增加代码复杂性:引入了新的类(适配器),可能会使代码结构变得更复杂,尤其是在适配逻辑很简单的情况下。
- 性能开销:适配器转换过程可能会带来一些额外的性能开销,但通常可以忽略不计。
1.3 适用场景
- 希望使用某个已经存在的类,但它的接口不符合你的需求。 这是最常见的场景。
- 想要创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
- 需要使用几个现有的子类,但通过对每个子类进行子类化来适配它们的接口是不现实的。 这种情况下,可以使用一个对象适配器来适配它们的父类接口。
1.4 适配器模式的UML图与核心角色
适配器模式主要包含以下三个核心角色:
- Client(客户端):与符合
Target接口的对象协同工作。 - Target(目标接口):客户端所期望的接口。
- Adaptee(被适配者):一个已经存在的、接口不兼容的类,需要被适配。
- Adapter(适配器):实现
Target接口,并持有一个Adaptee的实例。它将Target接口的调用转换为对Adaptee接口的调用。
UML 类图:
+---------+ +----------------+ +-----------------+
| Client |------>| Target |<------| Adapter |
+---------+ +-----------------+ | (implements) || + Request() |<-----+-----------------++-----------------+ | - adaptee: Adaptee|+-----------------+| + Request() |+-----------------+| usesv+-------------+| Adaptee |+-------------+| + SpecificRequest() |+-------------+
二、 Go语言实现:对象适配器
对象适配器是最常用的一种实现方式。它通过组合(持有被适配者的实例)来实现适配,而不是通过继承。这完全符合Go语言“组合优于继承”的哲学。
我们用一个简单的例子来说明:我们有一个 LegacyPrinter(被适配者),它只有一个 PrintOld 方法。但我们的新系统(客户端)期望使用一个 ModernPrinter 接口(目标接口),该接口有一个 Print 方法。
第1步:定义目标接口
// Target: 现代打印机接口,客户端期望的接口
type ModernPrinter interface {Print(msg string)
}
第2步:定义被适配者
// Adaptee: 一个旧的打印机,接口不兼容
type LegacyPrinter struct{}
func (lp *LegacyPrinter) PrintOld(s string) {fmt.Println("Legacy Printer:", s)
}
第3步:创建适配器
// Adapter: 适配器,将旧打印机适配成现代打印机接口
type LegacyPrinterAdapter struct {legacyPrinter *LegacyPrinter
}
// NewLegacyPrinterAdapter 是适配器的构造函数
func NewLegacyPrinterAdapter(lp *LegacyPrinter) *LegacyPrinterAdapter {return &LegacyPrinterAdapter{legacyPrinter: lp}
}
// Print 实现了 ModernPrinter 接口
func (lpa *LegacyPrinterAdapter) Print(msg string) {// 在这里可以进行一些转换逻辑formattedMsg := "Adapter: " + msg// 调用被适配者的方法lpa.legacyPrinter.PrintOld(formattedMsg)
}
第4步:客户端使用
func main() {// 1. 创建一个被适配者实例oldPrinter := &LegacyPrinter{}// 2. 创建适配器,并将被适配者注入adapter := NewLegacyPrinterAdapter(oldPrinter)// 3. 客户端通过目标接口来使用适配器// 客户端不需要知道它实际在和 LegacyPrinter 打交道printMessage(adapter, "Hello, World!")
}
// Client: 一个函数,它只关心 ModernPrinter 接口
func printMessage(p ModernPrinter, msg string) {p.Print(msg)
}
对象适配器模式(完整版)
这是最常用、最推荐的实现方式。 文件:object_adapter.go
package main
import "fmt"
// ======================
// 1. 定义目标接口
// ======================
// Target: 现代打印机接口,客户端期望的接口
type ModernPrinter interface {Print(msg string)
}
// ======================
// 2. 定义被适配者
// ======================
// Adaptee: 一个旧的打印机,接口不兼容
type LegacyPrinter struct{}
func (lp *LegacyPrinter) PrintOld(s string) {fmt.Println("Legacy Printer:", s)
}
// ======================
// 3. 创建适配器
// ======================
// Adapter: 适配器,将旧打印机适配成现代打印机接口
type LegacyPrinterAdapter struct {legacyPrinter *LegacyPrinter
}
// NewLegacyPrinterAdapter 是适配器的构造函数
func NewLegacyPrinterAdapter(lp *LegacyPrinter) *LegacyPrinterAdapter {return &LegacyPrinterAdapter{legacyPrinter: lp}
}
// Print 实现了 ModernPrinter 接口
func (lpa *LegacyPrinterAdapter) Print(msg string) {// 在这里可以进行一些转换逻辑formattedMsg := "Adapter: " + msg// 调用被适配者的方法lpa.legacyPrinter.PrintOld(formattedMsg)
}
// ======================
// 4. 客户端使用
// ======================
// Client: 一个函数,它只关心 ModernPrinter 接口
func printMessage(p ModernPrinter, msg string) {p.Print(msg)
}
func main() {fmt.Println("--- Running Object Adapter Example ---")// 1. 创建一个被适配者实例oldPrinter := &LegacyPrinter{}// 2. 创建适配器,并将被适配者注入adapter := NewLegacyPrinterAdapter(oldPrinter)// 3. 客户端通过目标接口来使用适配器// 客户端不需要知道它实际在和 LegacyPrinter 打交道printMessage(adapter, "Hello, World!")
}
执行结果:
$ go run object_adapter.go
--- Running Object Adapter Example ---
Legacy Printer: Adapter: Hello, World!
在这个例子中,printMessage 函数(客户端)成功调用了 LegacyPrinter 的功能,而这一切都通过 LegacyPrinterAdapter 这个“中间人”无缝地完成了。
三、Go语言实现:类适配器
类适配器通过多重继承来实现。在Go语言中,没有传统的类继承,但我们可以通过结构体嵌入来模拟类似的效果。
类适配器会同时继承(嵌入)Target 接口和 Adaptee 结构体。这种方式在Go中比较少见,因为它不如对象适配器灵活。
我们继续用上面的例子。
第1步和第2步:目标接口和被适配者(同上)
// Target
type ModernPrinter interface {Print(msg string)
}
// Adaptee
type LegacyPrinter struct{}
func (lp *LegacyPrinter) PrintOld(s string) {fmt.Println("Legacy Printer:", s)
}
第3步:创建类适配器(通过嵌入)
// Adapter: 通过嵌入来模拟类适配器
// 注意:这种写法在Go中并不常见,也不够灵活
type ClassAdapter struct {*LegacyPrinter // 嵌入被适配者,相当于“继承”了它的方法
}
// Print 实现了 ModernPrinter 接口
func (ca *ClassAdapter) Print(msg string) {// 直接调用嵌入结构体的方法ca.PrintOld("Class Adapter: " + msg)
}
第4步:客户端使用
func main() {// 直接创建类适配器实例classAdapter := &ClassAdapter{}// 客户端代码与之前完全一样printMessage(classAdapter, "Hello from Class Adapter!")
}
func printMessage(p ModernPrinter, msg string) {p.Print(msg)
}
类适配器模式(完整版)
这是通过结构体嵌入模拟的实现方式,在Go中不常用。
文件:class_adapter.go
package main
import "fmt"
// ======================
// 1. 定义目标接口
// ======================
// Target: 现代打印机接口,客户端期望的接口
type ModernPrinter interface {Print(msg string)
}
// ======================
// 2. 定义被适配者
// ======================
// Adaptee: 一个旧的打印机,接口不兼容
type LegacyPrinter struct{}
func (lp *LegacyPrinter) PrintOld(s string) {fmt.Println("Legacy Printer:", s)
}
// ======================
// 3. 创建类适配器 (通过嵌入)
// ======================
// Adapter: 通过嵌入来模拟类适配器
// 注意:这种写法在Go中并不常见,也不够灵活
type ClassAdapter struct {*LegacyPrinter // 嵌入被适配者,相当于“继承”了它的方法
}
// Print 实现了 ModernPrinter 接口
func (ca *ClassAdapter) Print(msg string) {// 直接调用嵌入结构体的方法ca.PrintOld("Class Adapter: " + msg)
}
// ======================
// 4. 客户端使用
// ======================
// Client: 一个函数,它只关心 ModernPrinter 接口
func printMessage(p ModernPrinter, msg string) {p.Print(msg)
}
func main() {fmt.Println("--- Running Class Adapter Example ---")// 直接创建类适配器实例classAdapter := &ClassAdapter{}// 客户端代码与之前完全一样printMessage(classAdapter, "Hello from Class Adapter!")
}
执行结果:
$ go run class_adapter.go
--- Running Class Adapter Example ---
Legacy Printer: Class Adapter: Hello from Class Adapter!
为什么类适配器在Go中不常用?
- 耦合度高:适配器与被适配者静态地绑定在一起,无法在运行时更换被适配者。
- 灵活性差:对象适配器可以适配
LegacyPrinter的任何子类,而类适配器则做不到。 - 不符合Go哲学:Go推崇组合和显式依赖注入,而类适配器的嵌入方式更像是一种“继承”的变体,不够清晰。
四、Go语言实现:一个更实际的例子
假设我们正在开发一个日志系统,我们有一个统一的 Logger 接口(目标)。现在,我们想集成一个第三方的日志库 ThirdPartyLogger(被适配者),但它的接口是 Log。
第1步:定义目标接口
// Target: 我们系统统一的日志接口
type Logger interface {Info(message string)
}
第2步:定义被适配者(第三方库)
// Adaptee: 第三方日志库,我们无法修改它的代码
type ThirdPartyLogger struct{}
func (tpl *ThirdPartyLogger) Log(level, message string) {fmt.Printf("[ThirdParty] %s: %s\n", level, message)
}
第3步:创建适配器
// Adapter: 将第三方日志库适配到我们的Logger接口
type ThirdPartyLoggerAdapter struct {thirdPartyLogger *ThirdPartyLogger
}
func NewThirdPartyLoggerAdapter(tpl *ThirdPartyLogger) *ThirdPartyLoggerAdapter {return &ThirdPartyLoggerAdapter{thirdPartyLogger: tpl}
}
// Info 实现了我们的 Logger 接口
func (tpla *ThirdPartyLoggerAdapter) Info(message string) {// 将我们的 Info 调用转换为第三方的 Log 调用tpla.thirdPartyLogger.Log("INFO", message)
}
第4步:客户端使用
func main() {// 我们的系统需要使用 Logger 接口var logger Logger// 现在我们想用第三方库,通过适配器进行适配thirdPartyLogger := &ThirdPartyLogger{}logger = NewThirdPartyLoggerAdapter(thirdPartyLogger)// 系统的其余部分可以无差别地使用 loggerLogInfo(logger, "System started successfully.")
}
// Client: 系统中的一个函数,它只依赖 Logger 接口
func LogInfo(l Logger, msg string) {l.Info(msg)
}
实际应用示例:日志系统适配器(完整版)
这个例子更贴近真实世界的开发场景。
文件:logger_adapter.go
package main
import "fmt"
// ======================
// 1. 定义目标接口
// ======================
// Target: 我们系统统一的日志接口
type Logger interface {Info(message string)
}
// ======================
// 2. 定义被适配者 (第三方库)
// ======================
// Adaptee: 第三方日志库,我们无法修改它的代码
type ThirdPartyLogger struct{}
func (tpl *ThirdPartyLogger) Log(level, message string) {fmt.Printf("[ThirdParty] %s: %s\n", level, message)
}
// ======================
// 3. 创建适配器
// ======================
// Adapter: 将第三方日志库适配到我们的Logger接口
type ThirdPartyLoggerAdapter struct {thirdPartyLogger *ThirdPartyLogger
}
func NewThirdPartyLoggerAdapter(tpl *ThirdPartyLogger) *ThirdPartyLoggerAdapter {return &ThirdPartyLoggerAdapter{thirdPartyLogger: tpl}
}
// Info 实现了我们的 Logger 接口
func (tpla *ThirdPartyLoggerAdapter) Info(message string) {// 将我们的 Info 调用转换为第三方的 Log 调用tpla.thirdPartyLogger.Log("INFO", message)
}
// ======================
// 4. 客户端使用
// ======================
// Client: 系统中的一个函数,它只依赖 Logger 接口
func LogInfo(l Logger, msg string) {l.Info(msg)
}
func main() {fmt.Println("--- Running Real-World Logger Adapter Example ---")// 我们的系统需要使用 Logger 接口var logger Logger// 现在我们想用第三方库,通过适配器进行适配thirdPartyLogger := &ThirdPartyLogger{}logger = NewThirdPartyLoggerAdapter(thirdPartyLogger)// 系统的其余部分可以无差别地使用 loggerLogInfo(logger, "System started successfully.")
}
执行结果:
$ go run logger_adapter.go
--- Running Real-World Logger Adapter Example ---
[ThirdParty] INFO: System started successfully.
这个例子完美地展示了适配器模式的实际价值:在不修改现有代码(包括客户端和第三方库)的情况下,让两者协同工作。
总结:适配器模式是一个非常实用且常见的设计模式,它的核心是转换接口,解决不兼容问题。在Go语言中:
- 对象适配器是首选实现方式。它利用Go的组合特性,将适配器与被适配者解耦,非常灵活,符合Go的编程哲学。
- 类适配器可以通过结构体嵌入来模拟,但由于其高耦合和低灵活性,在实际Go开发中很少使用。
- 函数式适配器:在某些简单场景下,适配器甚至可以是一个函数。例如,
func AdapterFunc(s string) { oldPrinter.PrintOld(s) }。如果目标接口只有一个方法,这种方式非常简洁。
当你遇到以下情况时,应该首先想到适配器模式:
- 集成第三方库或遗留系统。
- 需要统一多个不同接口的类,让它们能被客户端以同样的方式处理。
掌握适配器模式,能让你在处理系统集成和接口兼容性问题时更加得心应手。
