适配器模式
本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 92 课,共 107 课。
适配器模式通过用新接口包装现有类型,让不兼容的接口能够协同工作。命令模式封装操作,而适配器会将一个接口转换为客户端所需的另一个接口。
假设你有一台旧式打印机,它使用的方法签名与你的应用程序预期的不同:
// 你的应用使用的目标接口
type Printer interface {
Print(text string) string
}
// 接口不兼容的旧版打印机
type OldPrinter struct{}
func (o OldPrinter) PrintDocument(doc string, copies int) string {
return fmt.Sprintf("Old printer: %s (x%d)", doc, copies)
}适配器封装旧类型,并实现预期的接口:
type OldPrinterAdapter struct {
oldPrinter OldPrinter
}
func (a OldPrinterAdapter) Print(text string) string {
return a.oldPrinter.PrintDocument(text, 1)
}现在,这台旧式打印机可以与需要 Printer 接口的代码无缝配合:
func PrintMessage(p Printer, msg string) string {
return p.Print(msg)
}
adapter := OldPrinterAdapter{oldPrinter: OldPrinter{}}
result := PrintMessage(adapter, "Hello") // "Old printer: Hello (x1)"在集成第三方库、处理旧代码,或需要使用不符合接口要求的现有类时,适配器模式非常有用。它可以充当桥梁,而无需修改原始代码。
挑战
简单让我们使用适配器模式构建一个媒体播放器系统!你有一个需要统一接口的现代音频播放器,但还需要集成使用完全不同方法签名的旧式音频设备。适配器可以弥合这个差异,让旧硬件也能与新系统无缝协作。
你会把代码分成 3 个文件:
player.go:定义目标接口和旧式音频设备。创建现代系统所需的
AudioPlayer接口,其中包含方法Play(track string) string,该方法接收曲目名称并返回播放消息。还要定义一个与此接口不匹配的旧式设备:
VinylPlayer结构体,带有SpeedRPM字段(int)。它有一个方法DropNeedle(record string, side string) string,返回Playing [record] ([side] side) at [speed]rpm
VinylPlayer的接口与AudioPlayer不兼容。它需要两个参数,而且方法名称完全不同。adapter.go:创建适配器,让旧式设备兼容。创建一个
VinylAdapter结构体,用它包装VinylPlayer并实现AudioPlayer接口。调用Play时,适配器应调用黑胶播放器的DropNeedle方法,并将侧面参数默认设为A。再创建一个现代播放器作为对比:
DigitalPlayer结构体:它的Play方法返回Streaming: [track]
main.go:演示两个播放器如何通过同一个接口工作。创建函数
PlayMusic(player AudioPlayer, track string) string,通过接口播放音乐。这个函数不需要知道或关心使用的是数字播放器还是经过适配的黑胶播放器。读取播放器类型(
digital或vinyl),如果是黑胶播放器,还要读取 RPM 转速。然后读取曲目名称。创建相应的播放器(黑胶播放器使用适配器),通过你的PlayMusic函数播放曲目,并打印结果。
将提供以下输入:
- 第 1 行:播放器类型(
digital或vinyl) - 第 2 行:RPM 转速(仅当类型为
vinyl时) - 最后一行:要播放的曲目名称
例如,给定:
digital
Bohemian Rhapsody你的输出应为:
Streaming: Bohemian Rhapsody再例如,给定:
vinyl
33
Abbey Road你的输出应为:
Playing Abbey Road (A side) at 33rpm再例如,给定:
vinyl
45
Blue Monday你的输出应为:
Playing Blue Monday (A side) at 45rpm注意,PlayMusic 函数对两个播放器的工作方式完全相同。它完全不知道黑胶播放器实际上是通过适配器从一个完全不同的接口转换而来的。适配器会在幕后将现代接口调用转换为旧式格式!
亲手试试
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// PlayMusic 使用 AudioPlayer 接口播放音乐
// 这个函数适用于任何 AudioPlayer 实现
// TODO:实现这个函数,调用 player.Play(track) 并返回结果
func PlayMusic(player AudioPlayer, track string) string {
// TODO:实现这个函数
return ""
}
func main() {
reader := bufio.NewReader(os.Stdin)
// 读取播放器类型
playerType, _ := reader.ReadString('\n')
playerType = strings.TrimSpace(playerType)
var player AudioPlayer
var track string
if playerType == "vinyl" {
// 读取黑胶唱片的 RPM 转速
rpmStr, _ := reader.ReadString('\n')
rpmStr = strings.TrimSpace(rpmStr)
rpm, _ := strconv.Atoi(rpmStr)
// 读取曲目名称
track, _ = reader.ReadString('\n')
track = strings.TrimSpace(track)
// TODO:用给定的 RPM 创建 VinylPlayer
// TODO:用 VinylAdapter 包装它
// TODO:赋值给 player 变量
_ = rpm // 使用 rpm 后删除这一行
} else {
// 读取曲目名称
track, _ = reader.ReadString('\n')
track = strings.TrimSpace(track)
// TODO:创建 DigitalPlayer
// TODO:赋值给 player 变量
}
// TODO: 调用 PlayMusic 并打印结果
_ = player // 使用 player 时删除这一行
_ = track // 使用 track 时删除这一行
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器