将一个类的实现的接口转换成客户希望的另一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 适配器模式可以看做一种"补偿模式",用来补救设计上的缺陷。

适配器模式

使用场景

  • 封装有缺陷的接口设计
    • 如果引入的外部系统接口设计方面有缺陷,会影响我们自身代码的可测性等,就可以考虑使用适配器模式,将引入的系统向我们自身系统设计上靠拢
  • 统一多个类的接口设计
    • 如果一个功能依赖多个外部系统,且这些外部系统的能力是相似的但接口不统一,可以使用适配器模式,依赖于继承、多态的特性,使调用方可以以聚合方式使用外部系统,提升代码扩展性
    • 一种不使用适配器的方法是,扩展每个子类,将缺少的功能添加到新的子类中。但是,必须在所有新子类中重复添加这些代码,这样会使得代码有坏味道。
  • 替换依赖的外部系统:如果一个功能有多个外部系统可供选择,我们可以定义一个Target接口,将外部系统适配为Target,这样就能利用多态性,实现外部系统的替换
  • 兼容老版本接口:老版本中功能A在新版本中被废弃,A将由B替代,为了不影响使用者,新版本中仍然会有A,但是其内部实现委托B执行
  • 适配不同格式的数据:有时数据来源不同,数据格式也不同,需要对数据做适配,改为统一格式后再处理,也可使用适配器模式

实现步骤

  • 有两个类的接口不兼容:
    • 一个无法修改 (通常是第三方、 遗留系统或者存在众多已有依赖的类) 的功能性服务类。
    • 一个或多个将受益于使用服务类的客户端类。
  • 声明客户端接口,描述客户端如何与服务交互。
  • 创建遵循客户端接口的适配器类。所有方法暂时都为空。
  • 在适配器类中添加一个成员变量用于保存对于服务对象的引用。
    • 通常情况下会通过构造函数对该成员变量进行初始化,但有时在调用其方法时将该变量传递给适配器会更方便。
  • 依次实现适配器类客户端接口的所有方法。适配器会将实际工作委派给服务对象,自身只负责接口或数据格式的转换。
  • 客户端必须通过客户端接口使用适配器。这样一来,就可以在不影响客户端代码的情况下修改或扩展适配器。

优缺点

优点

  • 单一职责原则。可以将接口或数据转换代码从程序主要业务逻辑中分离。
  • 开闭原则。只要客户端代码通过客户端接口与适配器进行交互,就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。

缺点

  • 代码整体复杂度增加,因为需要新增一系列接口和类。有时直接更改服务类使其与其他代码兼容会更简单。

与其它模式的关系

  • 桥接模式通常会于开发前期进行设计,能够将程序的各个部分独立开来以便开发。适配器模式通常在已有程序中使用,让相互不兼容的类能很好地合作。
  • 适配器可以对已有对象的接口进行修改,装饰模式则能在不改变对象接口的前提下强化对象功能。
    • 此外,装饰还支持递归组合,适配器则无法实现。
  • 适配器能为被封装对象提供不同的接口,代理模式能为对象提供相同的接口,装饰则能为对象提供加强的接口。
  • 外观模式为现有对象定义了一个新接口,适配器则会试图运用已有的接口。
    • 适配器通常只封装一个对象,外观通常会作用于整个对象子系统上。
  • 桥接状态模式策略模式(在某种程度上包括适配器)模式的接口非常相似
    • 实际上它们都基于组合模式——即将工作委派给其他对象,不过也各自解决了不同的问题。
    • 模式并不只是以特定方式组织代码的配方,还可以使用它们来和其他开发者讨论模式所解决的问题。

适配器模式和代理、装饰器、桥接模式有一定相似性

  • 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
  • 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
  • 适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
  • 桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

示例

这里有一段客户端代码, 用于接收一个对象(Lightning 接口)的部分功能,不过我们还有另一个名为 adaptee 的对象(Windows 笔记本),可通过不同的接口(USB 接口)实现相同的功能。

这就是适配器模式发挥作用的场景。 我们可以创建这样一个名为 adapter 的结构体:

  • 遵循符合客户端期望的相同接口 (Lightning 接口)
  • 可以适合被适配对象的方式对来自客户端的请求进行 “翻译”。 适配器能够接受来自 Lightning 连接器的信息, 并将其转换成 USB 格式的信号, 同时将信号传递给 Windows 笔记本的 USB 接口。

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// computer 客户端支持的接口
type computer interface {
	insertIntoLightningPort()
}

// client 使用服务的客户端
type client struct {
}

func (c *client) insertLightningConnectorIntoComputer(com computer) {
	fmt.Println("Client inserts Lightning connector into computer.")
	com.insertIntoLightningPort()
}
1
2
3
4
5
6
7
// mac 可以直接使用的服务
type mac struct {
}

func (m *mac) insertIntoLightningPort() {
	fmt.Println("Lightning connector is plugged into mac machine.")
}
1
2
3
4
5
6
7
// windows 待适配的服务
type windows struct {
}

func (w *windows) insertIntoUSBPort() {
	fmt.Println("USB connector is plugged into windows machine.")
}
1
2
3
4
5
6
7
8
9
// windowsAdapter windows的适配器
type windowsAdapter struct {
	windowsMachine *windows
}

func (w *windowsAdapter) insertIntoLightningPort() {
	fmt.Println("Adapter converts Lightning signal to USB.")
	w.windowsMachine.insertIntoUSBPort()
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

func main() {

	client := &client{}

	// mac可以直接使用client服务
	mac := &mac{}
	client.insertLightningConnectorIntoComputer(mac)

	// windows需要使用适配器,才能使用client服务
	windowsMachine := &windows{}
	windowsMachineAdapter := &windowsAdapter{
		windowsMachine: windowsMachine,
	}

	client.insertLightningConnectorIntoComputer(windowsMachineAdapter)
}

References

guru