适配器模式
Structural design patterns deal with the relationships between the entities (such as classes and objects) of a system. A structural design pattern focuses on providing a simple way of composing objects for creating new functionality [GOF95, page 155], [j.mp/structpat].
结构化设计模式处理一个系统的对象之间的关系。
Adapter is a structural design pattern that helps us make two incompatible interfaces compatible. First, let's answer what incompatible interfaces really mean. If we have an old component and we want to use it in a new system, or a new component that we want to use in an old system, the two can rarely communicate without requiring any code changes. But changing the code is not always possible, either because we don't have access to it (for example, the component is provided as an external library) or because it is impractical. In such cases, we can write an extra layer that makes all the required modi cations for enabling the communication between the two interfaces. This layer is called the Adapter.
适配器是一个有助于我们把两个不适配的结构变得可适配的结构化设计模式。首先,让我们来回答不适配借口的真实含义。如果我们拥有一个旧组件,而且我们想要把它用于一个新系统,或者是我们想要在一个旧系统中使用新组建,那么这两种方式下,很少有不要求不做任何代码改变的情况。但是改变代码也不少总是可行的,除非是因为我们不想访问他(例如,组建作为一个扩展库而被提供),或者是因为根本不可行。遇到这些情况,我们可以编写一个能够
E-commerce systems are known examples. Assume that we use an e-commerce system that contains a calculate_total(order) function. The function calculates the total amount of an order, but only in Danish Kroner (DKK). It is reasonable for our customers to ask us to add support for more popular currencies, such as United States Dollars (USD) and Euros (EUR). If we own the source code of the system we can extend it by adding new functions for doing the conversions from DKK to USD and from DKK to EUR. But what if we don't have access to the source code of the application because it is provided to us only as an external library? In this case, we can still use the library (for example, call its methods), but we cannot modify/extend it. The solution is to write a wrapper (also known as Adapter) that converts the data from the given DKK format to the expected USD or EUR format.
电商系统是一个已知的例子。假设我们使用一个包含了calculate_total(order)函数的电商系统。该函数封装了一个订单的总价,但是只用于丹麦克朗(DKK)。我们的客户要求我们添加对更多流行货币对支持是合理的,比如美国美元(USD),和欧元(EUR)。如果我们拥有系统的源代码,我们可以通过添加把DKK转换到USD的新函数来扩展系统。但是,如果我们没有访问应用的源代码权限,因为这个系统仅支持扩展库,该怎么办呢?遇到这种情况,我们仍旧使用库(例如,调用系统的方法),但是不能修改/扩展它。解决办法是编写一个包装器(也被称作适配器)把数据从给定的DKK格式转换到希望使用的USD或者EUR格式。
The Adapter pattern is not useful only for data conversions. In general, if you want to use an interface that expects function_a() but you only have function_b(), you can use an Adapter to convert (adapt) function_b() to function_a() [Eckel08, page 207], [j.mp/adapterpat]. This is not only true for functions but also for function parameters. An example is a function that expects the parameters x, y, and z but you only have a function that works with the parameters x and y at hand. We will see how to use the Adapter pattern in the implementation section.
适配器模式不仅仅只可用于数据转换。通常,如果你想要使用一个
A real-life example 真实事例
Probably all of us use the Adapter pattern every day, but in hardware instead of software. If you have a smartphone or a tablet, you need to use something (for example, the lightning connector of an iPhone) with a USB adapter for connecting it to your computer. If you are traveling from most European countries to the UK, you need to use a plug adapter for charging your laptop. The same is true if you are traveling from Europe to USA, or the other way around. Adapters are everywhere!
可能我们整天都在使用适配器模式,不过用的是硬件而不是软件。如果你拥有一部智能手机或者一台平板,那么你需要使用为了把设备连接到电脑而实用一个USB适配器。如果从欧洲的大多数国家启程去到英国旅行的话,你需要为你的笔记本电脑使用一个电源适配器。如果你从欧洲到美国旅行,或者周边国家时,也是同样的结果。适配器无处不在!
The following image, courtesy of sourcemaking.com, shows several examples of hardware adapters [j.mp/adapterpat]:
下面,由sourcemaking.com提供的图片,展示了硬件适配器的几个例子:
A software example 软件示例
Grok is a Python framework that runs on top of Zope 3 and focuses on agile development. The Grok framework uses Adapters for making it possible for existing objects to conform to speci c APIs without the need to modify them [j.mp/grokada].
Grok是一个运行在Zope 3基础支行的Python框架,它旨在于进行敏捷开发。Grok框架为了让现有项目是适应特定API而不对API修改,而使用了适配器。
The Python Traits package also uses the Adapter pattern for transforming an object that does not implement of a speci c interface (or set of interfaces) to an object that does [j.mp/pytraitsad].
Python的包Traits也为了把一个对象进行转换,而又不为这个对象实现专门的接口便使用适配器模式。
Use cases 用法案例
The Adapter pattern is used for making things work after they have been implemented [j.mp/adapterpat]. Usually one of the two incompatible interfaces is either foreign or old/legacy. If the interface is foreign, it means that we have no access to the source code. If it is old it is usually impractical to refactor it. We can take it even further and argue that altering the implementation of a legacy component to meet our needs is not only impractical, but it also violates the open/close principle [j.mp/adaptsimp]. The open/close principle is one of the fundamental principles of Object-Oriented design (the O of SOLID). It states that a software entity should be open for extension, but closed for modi cation. That basically means that we should be able to extend the behavior of an entity without making source code modi cations. Adapter respects the open/closed principle [j.mp/openclosedp].
适配器模式在实现之后用来让事务工作起来。通常两个不兼容的接口中的其中一个不是外部的,就是老旧的。如果接口是外部的,这意味着我们不能访问源代码。如果是旧的话,一般不实际的做法是重构它。我们进一步来探讨改变旧代码组建的实现来实现我们的需求不仅仅是不切实际的,而且这还违反了开发/关闭原则。开发/关闭原则是面向对象设计的一个基础原则。它声明了,软件对象应该为扩展而开发,且对修改进行关闭。这基本就是说,我们应该能够扩展一个对象的行为,而不用对源代码进行修改。适配器同样遵守开发/关闭原则。
Therefore, using an Adapter for making things work after they have been implemented is a better approach because it:
因此,请在有了更好的实现之后使用适配器,因为:
- Does not require access to the source code of the foreign interface
Does not violate the open/closed principle
不要求访问外部接口的源代码
- 不违反开放/关闭原则
Implementation 实现
There are many ways of implementing the Adapter design pattern in Python [Eckel08, page 207]. All the techniques demonstrated by Bruce Eckel use inheritance, but Python provides an alternative, and in my opinion, a more idiomatic way of implementing an Adapter. The alternative technique should be familiar to you, since it uses the internal dictionary of a class, and we have seen how to do that in Chapter 3, The Prototype Pattern.
在Python中有很多实现适配器模式的方法。所有的技术都由布鲁斯-埃寇使用继承来阐明,但是Python提供了一个选择,在我看来是一种更为理想的实现适配器的办法。另外一种技术你应该是熟悉的,因此它使用的是类的内部字典,而且在第三章-原型模式中,我们已经见过如何具体操作了。
Let's begin with the what we have part. Our application has a Computer class that shows basic information about a computer. All the classes of this example, including the Computer class are very primitive, because we want to focus on the Adapter pattern and not on how to make a class as complete as possible.
我们从自己所实现的部分开始。我们的应用拥有一个展示电脑基本信息的Computer类。该例子的所有类,包括Computer类在内都非常的简陋,因为我们想关注在适配模式上,而不是如何将一个类尽可能的编写完整。
class Computer:
def __init__(self, name):
self.name = name
def __str__(self):
return 'the {} computer'.format(self.name)
def execute(self):
return 'executes a program'
In this case, the execute() method is the main action that the computer can perform. This method is called by the client code.
在这种情况下,execute()方式是电脑可以执行主要动作。该方法被称作客户端代码。
Now we move to the what we want part. We decide to enrich our application with more functionality, and luckily, we nd two interesting classes implemented in two different libraries that are unrelated with our application: Synthesizer and Human. In the Synthesizer class, the main action is performed by the play() method. In the Human class, it is performed by the speak() method. To indicate that the two classes are external, we place them in a separate module, as shown:
现在,进入我们我们想要使用的代码。我们决定使用更多的功能扩展我们的应用,幸运的是,我们找到了两个有趣的使用不同和我们的应用无关的库实现的类:Synthesizer 和 Human。在Synthesizer,主要的行为是由play()方法执行的。在Human类中,主要是由speak()执行的。为了表明这两个类是来自外部的,我们把它放到一个独立的模块中,一如下所示:
class Synthesizer:
def __init__(self, name):
self.name = name
def __str__(self):
return 'the {} synthesizer'.format(self.name)
def play(self):
return 'is playing an electronic song'
class Human:
def __init__(self, name):
self.name = name
def __str__(self):
return '{} the human'.format(self.name)
def speak(self):
return 'says hello'
So far so good. But, we have a problem. The client only knows how to call the execute() method, and it has no idea about play() or speak(). How can we make the code work without changing the Synthesizer and Human classes? Adapters to the rescue! We create a generic Adapter class that allows us to adapt a number of objects with different interfaces, into one uni ed interface. The obj argument of the __init__()
method is the object that we want to adapt, and adapted_methods is a dictionary containing key/value pairs of method the client calls/method that should be called.
到目前为止还好。但是,我们有一个问题。用户仅知道如何调用execute()方法,而不知道如何使用play()或者speak()。我们如何让代码工作而又不改变类Synthesizer和Human?适配器就是拯救这个问题的!我们创建一个允许我们让不同的接口的多个对象适配到一个统一的接口的通用适配器类。__init__()
的obj参数是我们想要去适配的对象,而adapted_methods则是一个包含
class Adapter:
def __init__(self, obj, adapted_methods):
self.obj = obj
self.__dict__.update(adapted_methods)
def __str__(self):
return str(self.obj)
Let's see how we can use the Adapter pattern. An objects list holds all the objects. The compatible objects that belong to the Computer class need no adaptation. We can add them directly to the list. The incompatible objects are not added directly. They are adapted using the Adapter class. The result is that the client code can continue using the known execute() method on all objects without the need to be aware of any interface differences between the used classes.
让我们来看看如何使用适配器模式。一个对象列表拥有多个对象。属于Computer类的合适对象不需要去适配。我们直接把它们加入到列表。不合适的对象不会直接添加。它们要使用Adapter类进行适配。其结果是客户端代码可以继续在所有对象上使用 execute() 方法,而不需要关心所使用类之间任何接口上的不同。
def main():
objects = [Computer('Asus')]
synth = Synthesizer('moog')
objects.append(Adapter(synth, dict(execute=synth.play)))
human = Human('Bob')
objects.append(Adapter(human, dict(execute=human.speak)))
for i in objects:
print('{} {}'.format(str(i), i.execute()))
Let's see the complete code of the Adapter pattern example (files external.py and adapter.py) as follows:
如下,让我们看看适配器模式例子的完整代码文件(文件external.py和adapter.py):
class Synthesizer:
def __init__(self, name):
self.name = name
def __str__(self):
return 'the {} synthesizer'.format(self.name)
def play(self):
return 'is playing an electronic song'
class Human:
def __init__(self, name):
self.name = name
def __str__(self):
return '{} the human'.format(self.name)
def speak(self):
return 'says hello'
from external import Synthesizer, Human
class Computer:
def __init__(self, name):
self.name = name
def __str__(self):
return 'the {} computer'.format(self.name)
def execute(self):
return 'executes a program'
class Adapter:
def __init__(self, obj, adapted_methods):
self.obj = obj
self.__dict__.update(adapted_methods)
def __str__(self):
return str(self.obj)
def main():
objects = [Computer('Asus')]
synth = Synthesizer('moog')
objects.append(Adapter(synth, dict(execute=synth.play)))
human = Human('Bob')
objects.append(Adapter(human, dict(execute=human.speak)))
for i in objects:
print('{} {}'.format(str(i), i.execute()))
if __name__ == "__main__":
main()
The output when executing the example is:
执行这个例子输出结果是:
>>> python3 adapter.py
the Asus computer executes a program
the moog synthesizer is playing an electronic song
Bob the human says hello
We managed to make the Human and Synthesizer classes compatible with the interface expected by the client, without changing their source code. This is nice. Here's a challenging exercise for you. There is a problem with this implementation. While all classes have a name attribute, the following code fails:
我们希望由用户来管理
for i in objects:
print(i.name)
First of all, why does this code fail? Although this makes sense from a coding point of view, it does not make sense at all for the client code which should not be aware of details such as what is adapted and what is not adapted. We just want to provide a uniform interface. How can we make this code work?
首先要说的是,这个代码为何会运行失败呢?
Tip
Hint: Think of how you can delegate the non-adapted parts to the object contained in the Adapter class.
提示
想一想你该如何对包含在Adapter类中对象分配不适配的部分。
Summary 总结
This chapter covered the Adapter design pattern. We use the Adapter pattern for making two (or more) incompatible interfaces compatible. As a motivation, an e-commerce system that should support multiple currencies was mentioned. We use adapters every day for interconnecting devices, charging them, and so on.
本章讨论了适配器设计模式。我们使用适配器让两个(或者更多个)不兼容的接口变得兼容。从出发动机而言,一个电商系统应该支持早前提到的多种货币。我们每天都在使用适配器,比如为相互连接的设备进行充电,等等。
Adapter makes things work after they have been implemented. The Grok Python framework and the Traits package use the Adapter pattern for achieving API conformance and interface compatibility, respectively. The open/close principle is strongly connected with these aspects.
适配器在被实现后才可以让事务工作起来。Python框架Grok,和Traits包分别利用适配器模式实现API一致,和接口上的兼容性。
In the implementation section, we saw how to achieve interface conformance using the Adapter pattern without modifying the source code of the incompatible model. This is achieved through a generic Adapter class that does the work for us. Although we could use sub-classing (inheritance) to implement the Adapter pattern in the traditional way in Python, this technique is a great alternative.
在实现部分,我们见到了如何使用适配器模式实现接口一致,而不修改不兼容模型的源码。
In the next chapter, we will see how we can use the Decorator pattern to extend the behavior of an object without using sub-classing.
在下面的章节中,我们会看到如何使用装饰器模式扩展一个对象的行为,而不使用子类化。