适配器模式
参考书籍
go design patterns for real world project
介绍
旧的接口过期了,但替换旧的接口需要成本(如时间)。作为替代,创建一个新的接口,利用hood,去使用旧的接口
生活中插头的转换便是一个适配器。再例如type-c孔到USB孔的转接头,转接头就是一个适配器
优点:
- 具备可预测性。在我们的apps上地维护基于 open/closed 原则1,使得apps的行为更加容易预测
- 免修改。使用别人的功能,但不用对其进行修改
目标
两个不匹配的部分需要一起工作时可以考虑使用适配器模式(or facade pattern,for example)。如两个不匹配的接口需要一起工作,可以考虑适配器模式
在适配器对象上使用不兼容的接口
在下面的例子中,有一个旧的Printer接口和一个新的Printer接口。新的接口和旧的接口有不一样的函数签名(参数)。通过使用一个适配器,使用户仍旧能够使用旧的实现(要与一些遗留的代码一起工作)
要求和验收标准
我们有:
旧的接口 | 新的接口 |
---|---|
LegacyPrinter | ModernPrinter |
创建一个结构体实现:ModernPrinter接口并能且会使用(调用)LegacyPrinter接口。步骤如下:
- 创建一个适配器对象,实现ModernPrinter接口
- 这个新的适配器对象必须包含LegacyPrinter接口的实例
- 必须调用LegacyPrinter,并加上前缀字符串 Adapter.
单元测试我们的 Printer adapter
我们首先写下旧(legacy。后面我们会混合使用旧或者遗留,意识是一样的)代码,我们不会测试它,并想象这并不是我们的代码。
type LegacyPrinter interface {
Print(s string) string
}
type MyLegacyPrinter struct{}
func (l *MyLegacyPrinter) Print(s string) string {
newMsg := fmt.Sprintf("Legacy Printer:%s\n", s) //修改文本,添加前缀Legacy Printer:
println(newMsg)//输出结果
return newMsg// 返回修改后的字符
}
遗留的接口被称作LegacyPrinter并且有一个Print方法。MyLegacyPrint结构体实现了LegacyPrinter接口。
现在声明我们要改变的新的接口
type ModernPrinter interface {
PrintStored() string
}
在这个例子中,新的PrintStored方法并不会接收字符串作为参数,而是存储在实例中。我们会调用适配器模式的PrinterAdapter接口
type PrinterAdapter struct {
OldPrinter LegacyPrinter
Msg string
}
func (p *PrinterAdapter) PrintStored() (newMsg string) {
//实现内容会在后面展示
return
}
像之前提到的,PrinterAdapter适配器必须拥有字段用于存储待打印的字符。它还必须拥有一个字段来存储LegacyPrinter适配器的实例。所以我们的单元测试如下:
func TestAdapter(t *testing.T) {
//我们将使用Hello World!作为适配器的消息
msg := "Hello World!"
//我们创建了一个PrinterAdapter的实例叫做adapter
//传递了MyLegacyPrinter结构体的实例,作为LegacyPrinter字段,称为OldPrinter
//我们还传递了我们需要的消息(Msg)
adapter := PrinterAdapter{OldPrinter: &MyLegacyPrinter{}, Msg: msg}
//接着,使用ModernPrinter 接口的PrintStored方法,该方法并不需要参数,改方法会返回修改后的字符
//我们知道,MyLegacyPrinter结会体返回传递的字符加上前缀LegacyPrinter:,适配器会添加前缀Adapter:,
returnedMsg := adapter.PrintStored()
//所以,最终我们的文本需要满足:"Legacy Printer:Adapter:Hello World!\n"
if returnedMsg != "Legacy Printer:Adapter:Hello World!\n" {
t.Errorf("Message didn't match:%s\n", returnedMsg)
}
//我们正在传递一个接口的实例,我们需要考虑指针为空的情况。所以我们增加了下面的测试
adapter = PrinterAdapter{OldPrinter: nil, Msg: msg}
returnedMsg = adapter.PrintStored()
if returnedMsg != "Hello World!" {
t.Errorf("Message didn't match:%s", returnedMsg)
}
}
实现Implementation
type PrinterAdapter struct {
OldPrinter LegacyPrinter
Msg string
}
func (p *PrinterAdapter) PrintStored() (newMsg string) {
//检查是否有LegacyPrinter实例
if p.OldPrinter != nil {
//添加Adapter前缀
newMsg = fmt.Sprintf("Adapter:%s", p.Msg)
//通过LegacyPrinter输出结果
newMsg = p.OldPrinter.Print(newMsg)
} else {
//如果没有实例直接返回存储的字符
return p.Msg
}
return
}
执行测试
最后所有的代码放在一起(adapter_test)
// file name:adapter_test
package main_test
import (
"fmt"
"testing"
)
type LegacyPrinter interface {
Print(s string) string
}
type MyLegacyPrinter struct{}
func (l *MyLegacyPrinter) Print(s string) string {
newMsg := fmt.Sprintf("Legacy Printer:%s\n", s)
println(newMsg)
return newMsg
}
type ModernPrinter interface {
PrintStored() string
}
type PrinterAdapter struct {
OldPrinter LegacyPrinter
Msg string
}
func (p *PrinterAdapter) PrintStored() (newMsg string) {
if p.OldPrinter != nil {
newMsg = fmt.Sprintf("Adapter:%s", p.Msg)
newMsg = p.OldPrinter.Print(newMsg)
} else {
return p.Msg
}
return
}
func TestAdapter(t *testing.T) {
msg := "Hello World!"
adapter := PrinterAdapter{OldPrinter: &MyLegacyPrinter{}, Msg: msg}
returnedMsg := adapter.PrintStored()
if returnedMsg != "Legacy Printer:Adapter:Hello World!\n" {
t.Errorf("Message didn't match:%s\n", returnedMsg)
}
//处理空指针
adapter = PrinterAdapter{OldPrinter: nil, Msg: msg}
returnedMsg = adapter.PrintStored()
if returnedMsg != "Hello World!" {
t.Errorf("Message didn't match:%s", returnedMsg)
}
}
执行代码
go test -v .
执行结果
在这里插入图片描述
什么是open/closed:
最初由Bertrand Meyer在Object-Oriented Software Construction一书中提出
代码应该对新功能开放,对修改关闭。1:代码应该具备可扩展性,而不是修改。避免修改源代码,是因为我们并不总是能够意识到对代码修改的潜在影响。2:请记住,只有通过使用设计模式和面向接口的编程,才可能实现代码的可扩展性 ↩︎