- 参考书籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》
设计模式用前须知
- 设计模式种一句出现频率非常高的话是,“ 在不改动。。。。的情况下, 实现。。。。的扩展“ 。
- 对于设计模式的学习者来说,充分思考这句话其实非常重要, 因为这句往往只对框架/ 工具包的设计才有真正的意义。因为框架和工具包存在的意义,就是为了让其他的程序员予以利用, 进行功能的扩展,而这种功能的扩展必须以不需要改动框架和工具包中代码为前提
- 对于应用程序的编写者, 从理论上来说, 所有的应用层级代码至少都是处于可编辑范围内的, 如果不细加考量, 就盲目使用较为复杂的设计模式, 反而会得不偿失, 毕竟灵活性的获得, 也是有代价的。
适配器模式(Adapter)
-
设计意图
- 把一个类的接口转换成另一个用户所期待的接口。 适配器使得原本接口并不相互适配的多个类一起工作起来。
-
举例
- 有时, 一个为了实现重用而设计的工具包/类库没有办法被重用,原因仅仅是它的接口,和应用程序所需要的接口不匹配。
- 考虑一个可以让用户绘制,并且在图表中拖放图形元素(线条, 多边形, 文本视图)的图形编辑器。 这个编辑器的核心抽象就是图形对象,他们可以被编辑, 同时可以在屏幕的不同位置被绘制。 这里假设图形类对象的抽象接口定义为Shape,图形编辑器为每一种图形元素定义了一个继承自Shape的子类 或实现了Shape接口, 例如线条类LineShape,多边形类PolyShape,
- 注: Shape 可以是一个接口, 也可以是一个抽象类, 这点可以因为具体的编程语言实现不同而有所区别, 例如Java 中Interface的概念对应到C++中,则是一个纯虚基类)
- 考虑一个可以让用户绘制,并且在图表中拖放图形元素(线条, 多边形, 文本视图)的图形编辑器。 这个编辑器的核心抽象就是图形对象,他们可以被编辑, 同时可以在屏幕的不同位置被绘制。 这里假设图形类对象的抽象接口定义为Shape,图形编辑器为每一种图形元素定义了一个继承自Shape的子类 或实现了Shape接口, 例如线条类LineShape,多边形类PolyShape,
- 这些基础的几何图形相对容易实现, 因为他们的绘制能力和编辑能力都是天然有限的。 但是对于一个可以显示和编辑文字的文本形状TextShape (请联想Word 中双击后可以在其中输入文字的形状),则难实现得多, 因为即便是非常基本的文本编辑都会涉及到复杂的屏幕更新操作和缓冲区管理。 同时, 一个现成的, 用于编写用户界面的工具包/类库 可能已经提供了一个较为复杂的TextView 类用于实现显示和编辑文本。
- 理想情况下, 我们希望可以重用TextView 类来实现TextShape类, 但是, 这个现成的类库设计的时候可能并没有把图形类Shape Class考虑进来, 所以我们没有办法可交换式地使用TextView 类和 Shape 类的对象。
- 如何才能使**“已经存在的”且“不相关的类”** TextView 可以在一个期待着Shape接口的应用程序中工作呢?
- 我们可以去更改TextView 类, 让它符合Shape 接口, 但前提是我们需要拥有该工具包的源代码。 但即便我们拥有了源代码, 为了能让工具包符合一个应用的接口而改变其源代码内容也很不合理。
- 更加合理的做法是: 定义一个TextShape 类, 使得它将TextView 的接口适配为Shape 的接口。 TextShape在这个场景下就扮演了一个适配器角色。 这可以通过两种方式实现:
- 同时继承Shape和TextView
- 这个是针对C++而言的, 对应到Java 中则是先继承TextView类, 然后实现Shape 接口。
- 问题: 对于Java 而言, 如果Shape 是一个抽象类怎么办?
- 将TextView 的实例组合在一个TextShape 对象中, 并且利用TextView 的方法去实现Shape 接口。
- 这两种方式则分别对应了适配器设计模式的两种版本, 类适配器和对象适配器。
- 同时继承Shape和TextView
- 有时, 一个为了实现重用而设计的工具包/类库没有办法被重用,原因仅仅是它的接口,和应用程序所需要的接口不匹配。
-
图例说明
- 图片中的空心三角箭头,代表着继承(extends)或实现(Implement)关系, 由继承者/实现者 指向 被继承者/被继承者。
- 图片中的实心三角箭头且箭头末尾没有圆圈的, 代表着单一的引用关系, 但是被引用的对象也有可能被其他对象引用。
- 图片中的实心三角箭头且箭头末尾有圆圈的, 代表着一对多的引用关系。
- 图片中的虚线实心三角箭头, 代表着创建或者实例化的关系。
- 图片中的末端有圆圈的虚线是一个对方法体内容用伪代码说明的关系
-
这张图展示的是对象适配器的使用方式。它展示了一个Shape中定义的方法BoundingBox(), 如何被转换, 最终由TextView的GetExtent的方法实现该接口。
-
通常, 适配器是负责实现“被适配者”(在本例中是TextView)没有提供的功能的 。 这一点可以从上图中很清晰的看出: 用户需要在图形编辑器中无差别地“拖拽”不同的Shape 对象到一个新的位置,但是TextView类并没有提供这种功能, 适配器TextShape 则通过实现CreateManipulator, 返回一个TextManipulator 实例, 以添加这个缺失的功能。
##适配器模式总结
-
适用场景
- 当你想使用一个现成的类, 但是这个类的接口和你所需要的接口并不一致。
- 当你想创建一个可以被重用的类, 目的是协同一些接口不想兼容的类共同工作。
- (对象适配器才能使用的场景) 你需要重用一些已经存在的子类, 但没有办法为了适配他们, 将他们全部继承。(在Java 中根本不允许多继承, 在C++中则有可能发生“继承冲突”)
-
类适配器
-
对象适配器
-
参与者
- 适配目标(Target)–> 例子中的Shape接口
- 定义了一个用户所需要的和应用领域相关的接口
- 用户(Client) --> 例子中的图形编辑器
- 按照适配目标定义的接口方式去使用对象
- 被适配者(Adaptee)–> 例子中的TextView
- 定义了一个已经存在的接口, 需要被适配 - 适配器(Adapter)–> 例子中的TextShape
- 将被适配者的接口适配到目标接口
- 适配目标(Target)–> 例子中的Shape接口
-
适用场景再次总结
- 适配器模式的核心在于, 你已经有了一套定义好的【InterFaceA,InterfaceB,InterfaceC】 用来组织自己的应用, 在实现的过程中发现,部分接口【InterfaceC】 对于特定的类【BadSubClass】不太容易实现。 此时, 你发现有一些现成的类库(可能是第三方类库)中一些现成的类【GoodSubClass】某种程度上具备了你定义好的接口所需要的功能, 但是【GoodSubClass】实现的是自己定义的一套接口【InterFace1, Interface2, Interface3】,你希望直接把那些现成的类拿过来稍加改动, 直接符合你所定义的接口。 此时便是应用适配器模式的时机