https://refactoringguru.cn/design-patterns/adapter

结构型模式-适配器模式

亦称: 封装器模式、Wrapper、Adapter

意图

适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。

问题

假如你正在开发一款股票市场监测程序, 它会从不同来源下载 XML 格式的股票数据, 然后向用户呈现出美观的图表。

在开发过程中, 你决定在程序中整合一个第三方智能分析函数库。 但是遇到了一个问题, 那就是分析函数库只兼容 JSON 格式的数据。

你无法 “直接” 使用分析函数库, 因为它所需的输入数据格式与你的程序不兼容

你可以修改程序库来支持 XML。 但是, 这可能需要修改部分依赖该程序库的现有代码。 甚至还有更糟糕的情况, 你可能根本没有程序库的源代码, 从而无法对其进行修改。

解决方案

你可以创建一个适配器。 这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。

适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。 例如, 你可以使用一个将所有数据转换为英制单位 (如英尺和英里) 的适配器封装运行于米和千米单位制中的对象。

适配器不仅可以转换不同格式的数据, 其还有助于采用不同接口的对象之间的合作。 它的运作方式如下:

  1. 适配器实现与其中一个现有对象兼容的接口。
  2. 现有对象可以使用该接口安全地调用适配器方法。
  3. 适配器方法被调用后将以另一个对象兼容的格式和顺序将请求传递给该对象。

有时你甚至可以创建一个双向适配器来实现双向转换调用。

让我们回到股票市场程序。 为了解决数据格式不兼容的问题, 你可以为分析函数库中的每个类创建将 XML 转换为 JSON 格式的适配器, 然后让客户端仅通过这些适配器来与函数库进行交流。 当某个适配器被调用时, 它会将传入的 XML 数据转换为 JSON 结构, 并将其传递给被封装分析对象的相应方法。

真实世界类比

出国旅行前后的旅行箱

如果你是第一次从美国到欧洲旅行, 那么在给笔记本充电时可能会大吃一惊。 不同国家的电源插头和插座标准不同。 美国插头和德国插座不匹配。 同时提供美国标准插座和欧洲标准插头的电源适配器可以解决你的难题。

适配器模式结构

对象适配器

实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。

  1. 客户端 (Client) 是包含当前程序业务逻辑的类。
  2. 客户端接口 (Client Interface) 描述了其他类与客户端代码合作时必须遵循的协议。
  3. 服务 (Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能。
  4. 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。
  5. 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。

类适配器

这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现, 例如 C++。

  1. 类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。

伪代码

下列适配器模式演示基于经典的 “方钉和圆孔” 问题。

让方钉适配圆孔

适配器假扮成一个圆钉 (Round­Peg), 其半径等于方钉 (Square­Peg) 横截面对角线的一半 (即能够容纳方钉的最小外接圆的半径)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 假设你有两个接口相互兼容的类:圆孔(Round­Hole)和圆钉(Round­Peg)。
class RoundHole is
constructor RoundHole(radius) { ... }

method getRadius() is
// 返回孔的半径。

method fits(peg: RoundPeg) is
return this.getRadius() >= peg.getRadius()

class RoundPeg is
constructor RoundPeg(radius) { ... }

method getRadius() is
// 返回钉子的半径。


// 但还有一个不兼容的类:方钉(Square­Peg)。
class SquarePeg is
constructor SquarePeg(width) { ... }

method getWidth() is
// 返回方钉的宽度。


// 适配器类让你能够将方钉放入圆孔中。它会对 RoundPeg 类进行扩展,以接收适
// 配器对象作为圆钉。
class SquarePegAdapter extends RoundPeg is
// 在实际情况中,适配器中会包含一个 SquarePeg 类的实例。
private field peg: SquarePeg

constructor SquarePegAdapter(peg: SquarePeg) is
this.peg = peg

method getRadius() is
// 适配器会假扮为一个圆钉,
// 其半径刚好能与适配器实际封装的方钉搭配起来。
return peg.getWidth() * Math.sqrt(2) / 2


// 客户端代码中的某个位置。
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true

small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // 此处无法编译(类型不一致)。

small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false

适配器模式适合应用场景

当你希望使用某个类,但是其接口与其他代码不兼容时,可以使用适配器类。

适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。

如果您需要复用这样一些类,他们处于同一个继承体系,并且他们又有了额外的一些共同的方法,但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。

你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道

将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。

实现方式

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

适配器模式优缺点

优点

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

缺点

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

与其他模式的关系

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

代码示例

适配器是一种结构型设计模式, 它能使不兼容的对象能够相互合作。

适配器可担任两个对象间的封装器, 它会接收对于一个对象的调用, 并将其转换为另一个对象可识别的格式和接口。

在 C# 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 适配器模式在 C# 代码中很常见。 基于一些遗留代码的系统常常会使用该模式。 在这种情况下, 适配器让遗留代码与现代的类得以相互合作。

识别方法: 适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。 当适配器的任何方法被调用时, 它会将参数转换为合适的格式, 然后将调用定向到其封装对象中的一个或多个方法。

概念示例

本例说明了适配器设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

Program.cs: 概念示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;

namespace RefactoringGuru.DesignPatterns.Adapter.Conceptual
{
// The Target defines the domain-specific interface used by the client code.
public interface ITarget
{
string GetRequest();
}

// The Adaptee contains some useful behavior, but its interface is
// incompatible with the existing client code. The Adaptee needs some
// adaptation before the client code can use it.
class Adaptee
{
public string GetSpecificRequest()
{
return "Specific request.";
}
}

// The Adapter makes the Adaptee's interface compatible with the Target's
// interface.
class Adapter : ITarget
{
private readonly Adaptee _adaptee;

public Adapter(Adaptee adaptee)
{
this._adaptee = adaptee;
}

public string GetRequest()
{
return $"This is '{this._adaptee.GetSpecificRequest()}'";
}
}

class Program
{
static void Main(string[] args)
{
Adaptee adaptee = new Adaptee();
ITarget target = new Adapter(adaptee);

Console.WriteLine("Adaptee interface is incompatible with the client.");
Console.WriteLine("But with adapter client can call it's method.");

Console.WriteLine(target.GetRequest());
}
}
}

Output.txt: 执行结果

1
2
3
Adaptee interface is incompatible with the client.
But with adapter client can call it's method.
This is 'Specific request.'

在 C++ 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 适配器模式在 C++ 代码中很常见。 基于一些遗留代码的系统常常会使用该模式。 在这种情况下, 适配器让遗留代码与现代的类得以相互合作。

识别方法: 适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。 当适配器的任何方法被调用时, 它会将参数转换为合适的格式, 然后将调用定向到其封装对象中的一个或多个方法。

概念示例

本例说明了适配器设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

main.cc: 概念示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* The Target defines the domain-specific interface used by the client code.
*/
class Target {
public:
virtual ~Target() = default;

virtual std::string Request() const {
return "Target: The default target's behavior.";
}
};

/**
* The Adaptee contains some useful behavior, but its interface is incompatible
* with the existing client code. The Adaptee needs some adaptation before the
* client code can use it.
*/
class Adaptee {
public:
std::string SpecificRequest() const {
return ".eetpadA eht fo roivaheb laicepS";
}
};

/**
* The Adapter makes the Adaptee's interface compatible with the Target's
* interface.
*/
class Adapter : public Target {
private:
Adaptee *adaptee_;

public:
Adapter(Adaptee *adaptee) : adaptee_(adaptee) {}
std::string Request() const override {
std::string to_reverse = this->adaptee_->SpecificRequest();
std::reverse(to_reverse.begin(), to_reverse.end());
return "Adapter: (TRANSLATED) " + to_reverse;
}
};

/**
* The client code supports all classes that follow the Target interface.
*/
void ClientCode(const Target *target) {
std::cout << target->Request();
}

int main() {
std::cout << "Client: I can work just fine with the Target objects:\n";
Target *target = new Target;
ClientCode(target);
std::cout << "\n\n";
Adaptee *adaptee = new Adaptee;
std::cout << "Client: The Adaptee class has a weird interface. See, I don't understand it:\n";
std::cout << "Adaptee: " << adaptee->SpecificRequest();
std::cout << "\n\n";
std::cout << "Client: But I can work with it via the Adapter:\n";
Adapter *adapter = new Adapter(adaptee);
ClientCode(adapter);
std::cout << "\n";

delete target;
delete adaptee;
delete adapter;

return 0;
}

Output.txt: 执行结果

1
2
3
4
5
6
7
8
Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

多重继承

在 C++ 中, 可以使用多重继承来实现适配器模式。

main.cc: 多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* The Target defines the domain-specific interface used by the client code.
*/
class Target {
public:
virtual ~Target() = default;
virtual std::string Request() const {
return "Target: The default target's behavior.";
}
};

/**
* The Adaptee contains some useful behavior, but its interface is incompatible
* with the existing client code. The Adaptee needs some adaptation before the
* client code can use it.
*/
class Adaptee {
public:
std::string SpecificRequest() const {
return ".eetpadA eht fo roivaheb laicepS";
}
};

/**
* The Adapter makes the Adaptee's interface compatible with the Target's
* interface using multiple inheritance.
*/
class Adapter : public Target, public Adaptee {
public:
Adapter() {}
std::string Request() const override {
std::string to_reverse = SpecificRequest();
std::reverse(to_reverse.begin(), to_reverse.end());
return "Adapter: (TRANSLATED) " + to_reverse;
}
};

/**
* The client code supports all classes that follow the Target interface.
*/
void ClientCode(const Target *target) {
std::cout << target->Request();
}

int main() {
std::cout << "Client: I can work just fine with the Target objects:\n";
Target *target = new Target;
ClientCode(target);
std::cout << "\n\n";
Adaptee *adaptee = new Adaptee;
std::cout << "Client: The Adaptee class has a weird interface. See, I don't understand it:\n";
std::cout << "Adaptee: " << adaptee->SpecificRequest();
std::cout << "\n\n";
std::cout << "Client: But I can work with it via the Adapter:\n";
Adapter *adapter = new Adapter;
ClientCode(adapter);
std::cout << "\n";

delete target;
delete adaptee;
delete adapter;

return 0;
}

Output.txt: 执行结果

1
2
3
4
5
6
7
8
Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

在 Java 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 适配器模式在 Java 代码中很常见。 基于一些遗留代码的系统常常会使用该模式。 在这种情况下, 适配器让遗留代码与现代的类得以相互合作。

Java 核心程序库中有一些标准的适配器:

识别方法: 适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。 当适配器的任何方法被调用时, 它会将参数转换为合适的格式, 然后将调用定向到其封装对象中的一个或多个方法。

让方钉适配圆孔

这个简单的例子展示了适配器如何让不兼容的对象相互合作。

round

round/RoundHole.java: 圆孔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package refactoring_guru.adapter.example.round;

/**
* RoundHoles are compatible with RoundPegs.
*/
public class RoundHole {
private double radius;

public RoundHole(double radius) {
this.radius = radius;
}

public double getRadius() {
return radius;
}

public boolean fits(RoundPeg peg) {
boolean result;
result = (this.getRadius() >= peg.getRadius());
return result;
}
}

round/RoundPeg.java: 圆钉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package refactoring_guru.adapter.example.round;

/**
* RoundPegs are compatible with RoundHoles.
*/
public class RoundPeg {
private double radius;

public RoundPeg() {}

public RoundPeg(double radius) {
this.radius = radius;
}

public double getRadius() {
return radius;
}
}

square

square/SquarePeg.java: 方钉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package refactoring_guru.adapter.example.square;

/**
* SquarePegs are not compatible with RoundHoles (they were implemented by
* previous development team). But we have to integrate them into our program.
*/
public class SquarePeg {
private double width;

public SquarePeg(double width) {
this.width = width;
}

public double getWidth() {
return width;
}

public double getSquare() {
double result;
result = Math.pow(this.width, 2);
return result;
}
}

adapters

adapters/SquarePegAdapter.java: 方钉到圆孔的适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package refactoring_guru.adapter.example.adapters;

import refactoring_guru.adapter.example.round.RoundPeg;
import refactoring_guru.adapter.example.square.SquarePeg;

/**
* Adapter allows fitting square pegs into round holes.
*/
public class SquarePegAdapter extends RoundPeg {
private SquarePeg peg;

public SquarePegAdapter(SquarePeg peg) {
this.peg = peg;
}

@Override
public double getRadius() {
double result;
// Calculate a minimum circle radius, which can fit this peg.
result = (Math.sqrt(Math.pow((peg.getWidth() / 2), 2) * 2));
return result;
}
}

Demo.java: 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package refactoring_guru.adapter.example;

import refactoring_guru.adapter.example.adapters.SquarePegAdapter;
import refactoring_guru.adapter.example.round.RoundHole;
import refactoring_guru.adapter.example.round.RoundPeg;
import refactoring_guru.adapter.example.square.SquarePeg;

/**
* Somewhere in client code...
*/
public class Demo {
public static void main(String[] args) {
// Round fits round, no surprise.
RoundHole hole = new RoundHole(5);
RoundPeg rpeg = new RoundPeg(5);
if (hole.fits(rpeg)) {
System.out.println("Round peg r5 fits round hole r5.");
}

SquarePeg smallSqPeg = new SquarePeg(2);
SquarePeg largeSqPeg = new SquarePeg(20);
// hole.fits(smallSqPeg); // Won't compile.

// Adapter solves the problem.
SquarePegAdapter smallSqPegAdapter = new SquarePegAdapter(smallSqPeg);
SquarePegAdapter largeSqPegAdapter = new SquarePegAdapter(largeSqPeg);
if (hole.fits(smallSqPegAdapter)) {
System.out.println("Square peg w2 fits round hole r5.");
}
if (!hole.fits(largeSqPegAdapter)) {
System.out.println("Square peg w20 does not fit into round hole r5.");
}
}
}

OutputDemo.txt: 执行结果

1
2
3
Round peg r5 fits round hole r5.
Square peg w2 fits round hole r5.
Square peg w20 does not fit into round hole r5.

在 PHP 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 适配器模式在 PHP 代码中很常见。 基于一些遗留代码的系统常常会使用该模式。 在这种情况下, 适配器让遗留代码与现代的类得以相互合作。

识别方法: 适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。 当适配器的任何方法被调用时, 它会将参数转换为合适的格式, 然后将调用定向到其封装对象中的一个或多个方法。

概念示例

本例说明了适配器设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 PHP 应用案例。

index.php: 概念示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php

namespace RefactoringGuru\Adapter\Conceptual;

/**
* The Target defines the domain-specific interface used by the client code.
*/
class Target
{
public function request(): string
{
return "Target: The default target's behavior.";
}
}

/**
* The Adaptee contains some useful behavior, but its interface is incompatible
* with the existing client code. The Adaptee needs some adaptation before the
* client code can use it.
*/
class Adaptee
{
public function specificRequest(): string
{
return ".eetpadA eht fo roivaheb laicepS";
}
}

/**
* The Adapter makes the Adaptee's interface compatible with the Target's
* interface.
*/
class Adapter extends Target
{
private $adaptee;

public function __construct(Adaptee $adaptee)
{
$this->adaptee = $adaptee;
}

public function request(): string
{
return "Adapter: (TRANSLATED) " . strrev($this->adaptee->specificRequest());
}
}

/**
* The client code supports all classes that follow the Target interface.
*/
function clientCode(Target $target)
{
echo $target->request();
}

echo "Client: I can work just fine with the Target objects:\n";
$target = new Target();
clientCode($target);
echo "\n\n";

$adaptee = new Adaptee();
echo "Client: The Adaptee class has a weird interface. See, I don't understand it:\n";
echo "Adaptee: " . $adaptee->specificRequest();
echo "\n\n";

echo "Client: But I can work with it via the Adapter:\n";
$adapter = new Adapter($adaptee);
clientCode($adapter);

Output.txt: 执行结果

1
2
3
4
5
6
7
8
Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

真实世界示例

适配器允许你使用第三方或遗留系统的类, 即使它们与你的代码不兼容。 例如, 你可以创建一系列特殊的封装器, 来让应用所发出的调用与第三方类所要求的接口与格式适配, 而无需重写应用的通知接口以使其支持每一个第三方服务 (如钉钉、 微信、 短信或其他任何服务)。

index.php: 真实世界示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<?php

namespace RefactoringGuru\Adapter\RealWorld;

/**
* The Target interface represents the interface that your application's classes
* already follow.
*/
interface Notification
{
public function send(string $title, string $message);
}

/**
* Here's an example of the existing class that follows the Target interface.
*
* The truth is that many real apps may not have this interface clearly defined.
* If you're in that boat, your best bet would be to extend the Adapter from one
* of your application's existing classes. If that's awkward (for instance,
* SlackNotification doesn't feel like a subclass of EmailNotification), then
* extracting an interface should be your first step.
*/
class EmailNotification implements Notification
{
private $adminEmail;

public function __construct(string $adminEmail)
{
$this->adminEmail = $adminEmail;
}

public function send(string $title, string $message): void
{
mail($this->adminEmail, $title, $message);
echo "Sent email with title '$title' to '{$this->adminEmail}' that says '$message'.";
}
}

/**
* The Adaptee is some useful class, incompatible with the Target interface. You
* can't just go in and change the code of the class to follow the Target
* interface, since the code might be provided by a 3rd-party library.
*/
class SlackApi
{
private $login;
private $apiKey;

public function __construct(string $login, string $apiKey)
{
$this->login = $login;
$this->apiKey = $apiKey;
}

public function logIn(): void
{
// Send authentication request to Slack web service.
echo "Logged in to a slack account '{$this->login}'.\n";
}

public function sendMessage(string $chatId, string $message): void
{
// Send message post request to Slack web service.
echo "Posted following message into the '$chatId' chat: '$message'.\n";
}
}

/**
* The Adapter is a class that links the Target interface and the Adaptee class.
* In this case, it allows the application to send notifications using Slack
* API.
*/
class SlackNotification implements Notification
{
private $slack;
private $chatId;

public function __construct(SlackApi $slack, string $chatId)
{
$this->slack = $slack;
$this->chatId = $chatId;
}

/**
* An Adapter is not only capable of adapting interfaces, but it can also
* convert incoming data to the format required by the Adaptee.
*/
public function send(string $title, string $message): void
{
$slackMessage = "#" . $title . "# " . strip_tags($message);
$this->slack->logIn();
$this->slack->sendMessage($this->chatId, $slackMessage);
}
}

/**
* The client code can work with any class that follows the Target interface.
*/
function clientCode(Notification $notification)
{
// ...

echo $notification->send("Website is down!",
"<strong style='color:red;font-size: 50px;'>Alert!</strong> " .
"Our website is not responding. Call admins and bring it up!");

// ...
}

echo "Client code is designed correctly and works with email notifications:\n";
$notification = new EmailNotification("developers@example.com");
clientCode($notification);
echo "\n\n";


echo "The same client code can work with other classes via adapter:\n";
$slackApi = new SlackApi("example.com", "XXXXXXXX");
$notification = new SlackNotification($slackApi, "Example.com Developers");
clientCode($notification);

Output.txt: 执行结果

1
2
3
4
5
6
Client code is designed correctly and works with email notifications:
Sent email with title 'Website is down!' to 'developers@example.com' that says '<strong style='color:red;font-size: 50px;'>Alert!</strong> Our website is not responding. Call admins and bring it up!'.

The same client code can work with other classes via adapter:
Logged in to a slack account 'example.com'.
Posted following message into the 'Example.com Developers' chat: '#Website is down!# Alert! Our website is not responding. Call admins and bring it up!'.

在 Python 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 适配器模式在 Python 代码中很常见。 基于一些遗留代码的系统常常会使用该模式。 在这种情况下, 适配器让遗留代码与现代的类得以相互合作。

识别方法: 适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。 当适配器的任何方法被调用时, 它会将参数转换为合适的格式, 然后将调用定向到其封装对象中的一个或多个方法。

概念示例

本例说明了适配器设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

main.py: 概念示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Target:
"""
The Target defines the domain-specific interface used by the client code.
"""

def request(self) -> str:
return "Target: The default target's behavior."


class Adaptee:
"""
The Adaptee contains some useful behavior, but its interface is incompatible
with the existing client code. The Adaptee needs some adaptation before the
client code can use it.
"""

def specific_request(self) -> str:
return ".eetpadA eht fo roivaheb laicepS"


class Adapter(Target, Adaptee):
"""
The Adapter makes the Adaptee's interface compatible with the Target's
interface via multiple inheritance.
"""

def request(self) -> str:
return f"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"


def client_code(target: "Target") -> None:
"""
The client code supports all classes that follow the Target interface.
"""

print(target.request(), end="")


if __name__ == "__main__":
print("Client: I can work just fine with the Target objects:")
target = Target()
client_code(target)
print("\n")

adaptee = Adaptee()
print("Client: The Adaptee class has a weird interface. "
"See, I don't understand it:")
print(f"Adaptee: {adaptee.specific_request()}", end="\n\n")

print("Client: But I can work with it via the Adapter:")
adapter = Adapter()
client_code(adapter)

Output.txt: 执行结果

1
2
3
4
5
6
7
8
Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

在 Ruby 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 适配器模式在 Ruby 代码中很常见。 基于一些遗留代码的系统常常会使用该模式。 在这种情况下, 适配器让遗留代码与现代的类得以相互合作。

识别方法: 适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。 当适配器的任何方法被调用时, 它会将参数转换为合适的格式, 然后将调用定向到其封装对象中的一个或多个方法。

概念示例

本例说明了适配器设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

main.rb: 概念示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# The Target defines the domain-specific interface used by the client code.
class Target
# @return [String]
def request
'Target: The default target\'s behavior.'
end
end

# The Adaptee contains some useful behavior, but its interface is incompatible
# with the existing client code. The Adaptee needs some adaptation before the
# client code can use it.
class Adaptee
# @return [String]
def specific_request
'.eetpadA eht fo roivaheb laicepS'
end
end

# The Adapter makes the Adaptee's interface compatible with the Target's
# interface.
class Adapter < Target
# @param [Adaptee] adaptee
def initialize(adaptee)
@adaptee = adaptee
end

def request
"Adapter: (TRANSLATED) #{@adaptee.specific_request.reverse!}"
end
end

# The client code supports all classes that follow the Target interface.
def client_code(target)
print target.request
end

puts 'Client: I can work just fine with the Target objects:'
target = Target.new
client_code(target)
puts "\n\n"

adaptee = Adaptee.new
puts 'Client: The Adaptee class has a weird interface. See, I don\'t understand it:'
puts "Adaptee: #{adaptee.specific_request}"
puts "\n"

puts 'Client: But I can work with it via the Adapter:'
adapter = Adapter.new(adaptee)
client_code(adapter)

output.txt: 执行结果

1
2
3
4
5
6
7
8
Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

在 Swift 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 适配器模式在 Swift 代码中很常见。 基于一些遗留代码的系统常常会使用该模式。 在这种情况下, 适配器让遗留代码与现代的类得以相互合作。

识别方法: 适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。 当适配器的任何方法被调用时, 它会将参数转换为合适的格式, 然后将调用定向到其封装对象中的一个或多个方法。

概念示例

本例说明了适配器设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 Swift 应用案例。

Example.swift: 概念示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import XCTest

/// The Target defines the domain-specific interface used by the client code.
class Target {

func request() -> String {
return "Target: The default target's behavior."
}
}

/// The Adaptee contains some useful behavior, but its interface is incompatible
/// with the existing client code. The Adaptee needs some adaptation before the
/// client code can use it.
class Adaptee {

public func specificRequest() -> String {
return ".eetpadA eht fo roivaheb laicepS"
}
}

/// The Adapter makes the Adaptee's interface compatible with the Target's
/// interface.
class Adapter: Target {

private var adaptee: Adaptee

init(_ adaptee: Adaptee) {
self.adaptee = adaptee
}

override func request() -> String {
return "Adapter: (TRANSLATED) " + adaptee.specificRequest().reversed()
}
}

/// The client code supports all classes that follow the Target interface.
class Client {
// ...
static func someClientCode(target: Target) {
print(target.request())
}
// ...
}

/// Let's see how it all works together.
class AdapterConceptual: XCTestCase {

func testAdapterConceptual() {
print("Client: I can work just fine with the Target objects:")
Client.someClientCode(target: Target())

let adaptee = Adaptee()
print("Client: The Adaptee class has a weird interface. See, I don't understand it:")
print("Adaptee: " + adaptee.specificRequest())

print("Client: But I can work with it via the Adapter:")
Client.someClientCode(target: Adapter(adaptee))
}
}

Output.txt: 执行结果

1
2
3
4
5
6
Client: I can work just fine with the Target objects:
Target: The default target's behavior.
Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS
Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

真实世界示例

Example.swift: 真实世界示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import XCTest
import UIKit

/// Adapter Design Pattern
///
/// Intent: Convert the interface of a class into the interface clients expect.
/// Adapter lets classes work together that couldn't work otherwise because of
/// incompatible interfaces.

class AdapterRealWorld: XCTestCase {

/// Example. Let's assume that our app perfectly works with Facebook
/// authorization. However, users ask you to add sign in via Twitter.
///
/// Unfortunately, Twitter SDK has a different authorization method.
///
/// Firstly, you have to create the new protocol 'AuthService' and insert
/// the authorization method of Facebook SDK.
///
/// Secondly, write an extension for Twitter SDK and implement methods of
/// AuthService protocol, just a simple redirect.
///
/// Thirdly, write an extension for Facebook SDK. You should not write any
/// code at this point as methods already implemented by Facebook SDK.
///
/// It just tells a compiler that both SDKs have the same interface.

func testAdapterRealWorld() {

print("Starting an authorization via Facebook")
startAuthorization(with: FacebookAuthSDK())

print("Starting an authorization via Twitter.")
startAuthorization(with: TwitterAuthSDK())
}

func startAuthorization(with service: AuthService) {

/// The current top view controller of the app
let topViewController = UIViewController()

service.presentAuthFlow(from: topViewController)
}
}

protocol AuthService {

func presentAuthFlow(from viewController: UIViewController)
}

class FacebookAuthSDK {

func presentAuthFlow(from viewController: UIViewController) {
/// Call SDK methods and pass a view controller
print("Facebook WebView has been shown.")
}
}

class TwitterAuthSDK {

func startAuthorization(with viewController: UIViewController) {
/// Call SDK methods and pass a view controller
print("Twitter WebView has been shown. Users will be happy :)")
}
}

extension TwitterAuthSDK: AuthService {

/// This is an adapter
///
/// Yeah, we are able to not create another class and just extend an
/// existing one

func presentAuthFlow(from viewController: UIViewController) {
print("The Adapter is called! Redirecting to the original method...")
self.startAuthorization(with: viewController)
}
}

extension FacebookAuthSDK: AuthService {
/// This extension just tells a compiler that both SDKs have the same
/// interface.
}

Output.txt: 执行结果

1
2
3
4
5
6
Starting an authorization via Facebook
Facebook WebView has been shown
///
Starting an authorization via Twitter
The Adapter is called! Redirecting to the original method...
Twitter WebView has been shown. Users will be happy :)

在 TypeScript 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 适配器模式在 TypeScript 代码中很常见。 基于一些遗留代码的系统常常会使用该模式。 在这种情况下, 适配器让遗留代码与现代的类得以相互合作。

识别方法: 适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。 当适配器的任何方法被调用时, 它会将参数转换为合适的格式, 然后将调用定向到其封装对象中的一个或多个方法。

概念示例

本例说明了适配器设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

index.ts: 概念示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* The Target defines the domain-specific interface used by the client code.
*/
class Target {
public request(): string {
return 'Target: The default target\'s behavior.';
}
}

/**
* The Adaptee contains some useful behavior, but its interface is incompatible
* with the existing client code. The Adaptee needs some adaptation before the
* client code can use it.
*/
class Adaptee {
public specificRequest(): string {
return '.eetpadA eht fo roivaheb laicepS';
}
}

/**
* The Adapter makes the Adaptee's interface compatible with the Target's
* interface.
*/
class Adapter extends Target {
private adaptee: Adaptee;

constructor(adaptee: Adaptee) {
super();
this.adaptee = adaptee;
}

public request(): string {
const result = this.adaptee.specificRequest().split('').reverse().join('');
return `Adapter: (TRANSLATED) ${result}`;
}
}

/**
* The client code supports all classes that follow the Target interface.
*/
function clientCode(target: Target) {
console.log(target.request());
}

console.log('Client: I can work just fine with the Target objects:');
const target = new Target();
clientCode(target);

console.log('');

const adaptee = new Adaptee();
console.log('Client: The Adaptee class has a weird interface. See, I don\'t understand it:');
console.log(`Adaptee: ${adaptee.specificRequest()}`);

console.log('');

console.log('Client: But I can work with it via the Adapter:');
const adapter = new Adapter(adaptee);
clientCode(adapter);

Output.txt: 执行结果

1
2
3
4
5
6
7
8
Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

概念示例

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

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

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

client.go: 客户端代码

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

type client struct {
}

func (c *client) insertLightningConnectorIntoComputer(com computer) {
fmt.Println("Client inserts Lightning connector into computer.")
com.insertIntoLightningPort()
}

computer.go: 客户端接口

1
2
3
4
5
package main

type computer interface {
insertIntoLightningPort()
}

mac.go: 服务

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

type mac struct {
}

func (m *mac) insertIntoLightningPort() {
fmt.Println("Lightning connector is plugged into mac machine.")
}

windows.go: 未知服务

1
2
3
4
5
6
7
8
9
package main

import "fmt"

type windows struct{}

func (w *windows) insertIntoUSBPort() {
fmt.Println("USB connector is plugged into windows machine.")
}

windowsAdapter.go: 适配器

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

type windowsAdapter struct {
windowMachine *windows
}

func (w *windowsAdapter) insertIntoLightningPort() {
fmt.Println("Adapter converts Lightning signal to USB.")
w.windowMachine.insertIntoUSBPort()
}

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

func main() {

client := &client{}
mac := &mac{}

client.insertLightningConnectorIntoComputer(mac)

windowsMachine := &windows{}
windowsMachineAdapter := &windowsAdapter{
windowMachine: windowsMachine,
}

client.insertLightningConnectorIntoComputer(windowsMachineAdapter)
}

output.txt: 执行结果

1
2
3
4
5
Client inserts Lightning connector into computer.
Lightning connector is plugged into mac machine.
Client inserts Lightning connector into computer.
Adapter converts Lightning signal to USB.
USB connector is plugged into windows machine.

根据: Golang By Example