初探适配器模式

代码链接

koonchen/design-patterns/adapter

要点

将一个类的接口转换成客户希望的另外一个接口。该模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

假设现在有一个绘图软件,我们可以绘制各种图形,并且这些图形可以进行编辑。所有的图形都实现于接口 Shape ,比如 LineShape 实现直线, PolygonShape 实现多边形,但是 TextShape 就没有这么简单了,我们要考虑到屏幕刷新和缓冲区管理的问题,我们假设在工具箱中已经有一个成品工具叫做 TextView 它完成了编辑和显示的工作,但此时, TextViewShape 对象不能互换。

adapter 模式的解决方案是创建一个新的 TextShape 子类,由它适配 TextViewShape 的接口。我们可以通过两种方法完成这件事:

  1. 实现 Shape 接口和继承 TextView ,做单实现单继承或双继承,这称为 类适配器
  2. TextView 实例作为 TextShape 的组成部分,使用 TextView 的接口实现 TextShape ,此时 TextViewTextShape 之间的关系是关联,然后实现 Shape 接口,做单实现,这称为 对象适配器 。结构看下类图:

cover

这里我们将 TextShape 称为 Adapter

Adapter 模式的使用情况:

  • 你想使用一个已经存在的类,而它的接口不符合你的需求。
  • 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作。
  • 你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以适配它们的接口。此时适配器可以适配它的父类接口

他有以下参与者:

  • Target ( Shape ) — 定义 Client 使用的与特定领域相关的接口。
  • Client ( DrawingEditor ) — 与符合 Target 接口的对象协同。
  • Adaptee ( TextView ) — 已经存在的接口,这个接口需要适配。
  • Adapter ( TextShape ) — 对 Adaptee 的接口和 Target 接口进行适配。

适配器结构已经呼之欲出了 ( 对象适配器 ) :

cover

事实上,对于适配器模式,还有另外一种的结构,称为类适配器:

cover


关于 对象适配器类适配器

他们的结构不同,造成了一些差别,类适配器:

  • 用一个具体的 Adapter 类对 Adaptee 和 Target 进行适配。结果是我们想要适配一个类以及它所有的子类是不能胜任的。
  • 使得 Adapter 可以重定义 Adaptee 的部分行为,因为 Adapter 此时是 Adaptee 的一个子类。
  • 仅仅引入了一个对象,并不需要额外的指针以间接得到 Adaptee。

对象适配器:

  • 允许一个 Adapter 与多个 Adaptee 同时工作,即 Adaptee 本身以及它的所有子类同时工作。
  • 使得重定义 Adaptee 的行为比较困难,需要生成并引用 Adaptee 的子类并引用这个子类而非 Adaptee 本身。

关于使用 Adapter 模式时需要考虑的其他一些因素

  • Adapter 的适配程度:各个 Adapter 对于 Adaptee 的接口和 Target 接口适配的工作量是不一样的,这取决于它们的相似程度。
  • 可插入的 Adapter :当其他的类使用一个类时,如果所需的假定条件越少,这个类就更具有可复用性。如果将接口适配构建为一个类,就不需要假设对其他的类可见的是一个相同的接口,换句话说,接口适配使得我们可以将自己的类加入到一些现有的系统中去,这些系统对这个类的接口可能有所不同。
  • 使用双向适配器提供透明操作:使用适配器的一个潜在问题是,它们不对所有的客户都透明。比如被适配的对象不再兼容 Adaptee 的接口,因此并不是所有 Adaptee 对象可以被使用的地方它都可以被使用。双向适配器提供了这样的透明性,在两个不同的客户需要用不同的方式查看同一个对象时,双向适配器尤其有用。 PS:所谓的双向适配器结构与类适配器相同,属于多重继承。

实现

在实现中有以下注意点:

在 c++ 实现时, Adapter 类应该采用公共方式继承 Target 类,并且用私有方式继承 Adaptee 类,因此 Adapter 类应该是 Target 的子类,但不是 Adaptee 的子类。有许多方法可以实现可插入的适配器。

这里有三种实现方法,首先,是为 Adaptee 找到一个窄的接口,即可用于适配的最小操作集。因为包含较少操作的窄接口相对容易进行适配:

  • 使用抽象操作:在父类 ( Target ) 中使用子类 ( Adapter ) 方法,而该方法可能与 Adaptee 存在某关系,比如关联。如下图所示:

cover

  • 使用代理对象:从下面的机构中可以发现,现在我们将 Client 和 Target 进行了分离, Adapter 仅继承自被委派的具体的 Target ,其他部分和抽象操作是一样的。

cover

  • 参数化适配器:在 Smalltalk 中可以用一个或多个模块对适配器进行参数化。一个模块可以适配一个请求,并且适配器可以为每个请求存储一个模块。在本例中,先将一个结构进行转换,另一个模块存储。这种方式相对子类化是更方便的。

cover

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
interface Target {
void request();
}

class Adaptee {
void specificRequest() {
System.out.println("Adaptee instance has been created");
}
}

class Adapter implements Target {
Adaptee adaptee = new Adaptee();
@Override
public void request() {
adaptee.specificRequest();
}
}

class Client {
void execute() {
Adapter adapter = new Adapter();
adapter.request();
}
}

public class AdapterClient {
public static void main(String args[]) {
Client client = new Client();
client.execute();
}
}

我们按照对象适配器完成了单实现的 Adapter 模式。

在实际使用中,我们常常会有这样的状况,我们先用接口实现一个类,这个类的方法在下一个需要实现的类中也要使用,再次从头创建比较浪费时间,此时,适配器模式很好地处理了这件事:

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
interface TargetPro {
void requestPro();
}

class AdapteePro {
void specificRequestPro() {
System.out.println("AdapteePro instance has been created");
}
}

class AdapterPro implements TargetPro {

AdapteePro adapteePro;

public AdapterPro() {
this.adapteePro = new AdapteePro();
}

@Override
public void requestPro() {
adapteePro.specificRequestPro();
}
}

class ClientPro implements TargetPro {
TargetPro targetPro;

public ClientPro(TargetPro targetPro) {
this.targetPro = targetPro;
}

@Override
public void requestPro() {
targetPro.requestPro();
}
}

public class AdapterProClient {
public static void main(String args[]) {
ClientPro client = new ClientPro(new AdapterPro());
client.requestPro();
}
}

现在,如果我们继续实现具体的 Target ,只需要使用已有的 Adapter 作为参数即可。

python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from abc import abstractmethod, ABCMeta

class Target(metaclass=ABCMeta):
@abstractmethod
def request(self): pass

class Adaptee(object):
def specificRequest(self):
print('"Adaptee instance has been created"')

class Adapter(Target):
def __init__(self):
self._adaptee = Adaptee()
def request(self):
self._adaptee.specificRequest()

if __name__ == '__main__':
adapter = Adapter()
adapter.request()

这样实现了基本的对象适配器。

参考 python-patterns/structural/adapter.py 。和 Smalltalk 类似,用 Python 也可以实现参数化的适配器。

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
class Dog(object):
def __init__(self):
self._name = "Dog"

def bark(self):
return "woof!"

class Cat(object):
def __init__(self):
self._name = "Cat"

def meow(self):
return "meow!"

class Human(object):
def __init__(self):
self._name = "Human"

def speak(self):
return "hello!"

class Adapter(object):
def __init__(self, obj, **kwargs):
self._obj = obj
self.__dict__.update(kwargs)

def __getattr__(self, item):
return getattr(self._obj, item)

def original_dict(self):
return self._obj.__dict__

if __name__ == '__main__':
dog = Dog()
objects = []
objects.append(Adapter(dog, make_noise = dog.bark()))
cat = Cat()
objects.append(Adapter(cat, make_noise = cat.meow()))
human = Human()
objects.append(Adapter(human, make_noise = human.speak()))
for obj in objects:
print(f'A {obj._name} goes {obj.make_noise}')

Brige 模式的结构和对象适配器类似,但是它们的出发点不同:

  • Brige 的目的是将接口和实现分离。
  • Adapter 的目的是改变一个已有对象的接口。

Decorator 模式增强了其他对象的功能而同时不改变它的接口,因此 Decorator 模式对应用的透明性比 Adapter 模式好,同时 Decorator 支持递归组合,而纯粹的 Adapter 无法实现这一点。

Proxy 模式在不改变它的接口的条件下,为另一个对象定义了一个代理。

这些,我们将在未来将一探究竟。