初探适配器模式
代码链接
koonchen/design-patterns/adapter
要点
将一个类的接口转换成客户希望的另外一个接口。该模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
假设现在有一个绘图软件,我们可以绘制各种图形,并且这些图形可以进行编辑。所有的图形都实现于接口 Shape
,比如 LineShape
实现直线, PolygonShape
实现多边形,但是 TextShape
就没有这么简单了,我们要考虑到屏幕刷新和缓冲区管理的问题,我们假设在工具箱中已经有一个成品工具叫做 TextView
它完成了编辑和显示的工作,但此时, TextView
和 Shape
对象不能互换。
adapter 模式的解决方案是创建一个新的 TextShape
子类,由它适配 TextView
和 Shape
的接口。我们可以通过两种方法完成这件事:
- 实现
Shape
接口和继承TextView
,做单实现单继承或双继承,这称为 类适配器 。 - 将
TextView
实例作为TextShape
的组成部分,使用TextView
的接口实现TextShape
,此时TextView
和TextShape
之间的关系是关联,然后实现Shape
接口,做单实现,这称为 对象适配器 。结构看下类图:
这里我们将 TextShape
称为 Adapter
。
Adapter 模式的使用情况:
- 你想使用一个已经存在的类,而它的接口不符合你的需求。
- 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作。
- 你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以适配它们的接口。此时适配器可以适配它的父类接口。
他有以下参与者:
- Target ( Shape ) — 定义 Client 使用的与特定领域相关的接口。
- Client ( DrawingEditor ) — 与符合 Target 接口的对象协同。
- Adaptee ( TextView ) — 已经存在的接口,这个接口需要适配。
- Adapter ( TextShape ) — 对 Adaptee 的接口和 Target 接口进行适配。
适配器结构已经呼之欲出了 ( 对象适配器 ) :
事实上,对于适配器模式,还有另外一种的结构,称为类适配器:
关于 对象适配器 和 类适配器 :
他们的结构不同,造成了一些差别,类适配器:
- 用一个具体的 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 存在某关系,比如关联。如下图所示:
- 使用代理对象:从下面的机构中可以发现,现在我们将 Client 和 Target 进行了分离, Adapter 仅继承自被委派的具体的 Target ,其他部分和抽象操作是一样的。
- 参数化适配器:在 Smalltalk 中可以用一个或多个模块对适配器进行参数化。一个模块可以适配一个请求,并且适配器可以为每个请求存储一个模块。在本例中,先将一个结构进行转换,另一个模块存储。这种方式相对子类化是更方便的。
java 实现
1 | interface Target { |
我们按照对象适配器完成了单实现的 Adapter 模式。
在实际使用中,我们常常会有这样的状况,我们先用接口实现一个类,这个类的方法在下一个需要实现的类中也要使用,再次从头创建比较浪费时间,此时,适配器模式很好地处理了这件事:
1 | interface TargetPro { |
现在,如果我们继续实现具体的 Target
,只需要使用已有的 Adapter
作为参数即可。
python 实现
1 | from abc import abstractmethod, ABCMeta |
这样实现了基本的对象适配器。
参考 python-patterns/structural/adapter.py 。和 Smalltalk 类似,用 Python 也可以实现参数化的适配器。
1 | class Dog(object): |
Brige 模式的结构和对象适配器类似,但是它们的出发点不同:
- Brige 的目的是将接口和实现分离。
- Adapter 的目的是改变一个已有对象的接口。
Decorator 模式增强了其他对象的功能而同时不改变它的接口,因此 Decorator 模式对应用的透明性比 Adapter 模式好,同时 Decorator 支持递归组合,而纯粹的 Adapter 无法实现这一点。
Proxy 模式在不改变它的接口的条件下,为另一个对象定义了一个代理。
这些,我们将在未来将一探究竟。