Design-Pattern-Adapter
创建型模式已经全部介绍完,接下来是结构型模式。
在以往的文中提过,结构型模式和行为型模式边界比较模糊。根据定义,结构型模式侧重于类和对象的组合以构建灵活且高效的结构,而行为型模式侧重于类和对象之间的通信和职责分配。但结构和行为本身就会相互影响,因此会存在模棱两可的分类。例如代理语义上是个行为,但是代理模式是结构模式;中介者组织了组件间的结构关系,但是中介者模式是行为模式。
结构模式和行为模式都涉及多个类或对象。通常来说,结构模式中的单个类通常是不完整的,需要通过特定结构,来组成完整的功能。而行为模式中的单个类通常是独立的,模式定义了这些类之间的交互。
此定义能提供一些解释,但也不严谨,存在反例。笔者认为对于设计模式分类过度纠结是钻牛角尖,在实际使用时也不会纠结如何辨别两者。
场景
假设现在有一段客户端代码,其中和A
类对象进行交互。根据业务需求,在代码中需要新增与B
类对象进行交互的需求。为了更形象,可以想象客户端代码是一个进行数据分析的功能,A
和B
类是某种特定格式的数据。客户端代码需要读取数据,对数据进行处理并展示结果。
由于B
类的交互方式与A
类不同,因此无法在不修改客户端代码的情况下对B
进行适配。
1 |
|
如果不想改变客户端代码,要使得代码能运行。直观的想法,可以在调用客户端方法前,把B
类对象转化为A
对象。这个方法通常定义在客户端所在的类中。如果代码中不止此客户端,还有很多位置都会用到这个方法,可以把这个转化方法设置为静态公共方法。后续如果还要兼容其他C
、D
类等,可以把这些转换方法都抽取到一个工具类中。
1 |
|
从面向接口编程的思想出发,客户端代码如果需要兼容功能相似的多个类,应该定义统一的接口,并将多个类都实现这个接口。在客户端代码中,通过接口定义的方法与这些类进行交互,实现代码通用性。所以另一种修改思路就是定义共同接口,并A
和B
中都实现这个接口,并将客户端改为使用接口作为参数类型。
1 |
|
这样的写法通常是最佳实践。但此处存在一个前提,对于B
类,我们有修改其代码的权限。具体来说我们修改B
类实现了InterfaceA
接口。但这个前提不是一定成立的。例如B
类代码在别处还有引用,修改可能会破坏其它功能;例如B
类来自某个第三方库。在这种场景下,就需要用到适配器模式了。
适配器模式
适配器模式中,新增了一个适配器(Adapter)类。适配器类实现了目标(Target)接口,并通过组合形式持有一个被适配对象(Adaptee)类/接口。
对于上文例子来说,目标接口就是InterfaceA
,被适配对象就是类B
。由此可以得出以下类关系图和适配器代码。
classDiagram
direction TB
class Client
class InterfaceA {
<< interface >>
+methodA() ResultA
}
Client --> InterfaceA
class BAdapter {
-B b
+methodA() ResultA
}
class B {
+methodB() ResultB
}
BAdapter ..|> InterfaceA
BAdapter --> B
1 |
|
此时,Client
中需要InterfaceA
接口的实现类,而我们希望支持B
类对象时,可以在传入前创建一个从B
类对象到InterfaceA
的适配器。
1 |
|
从代码形式看,使用适配器的方式,和上文中的调用前先转换的方式convertBToA
很像。区别是convertBToA
中返回的是具体的A
类对象,而客户端需要的也是A
类。但当客户端接受的是某个接口类型InterfaceA
,此时再把所有对象转换成A
类(实现了InterfaceA
),虽然也能满足功能,但总觉得像饶了原路,不符合面向接口编程的思想。
或者可以在转换方法中新建一个InterfaceA
的匿名实现类对象(匿名实现类通常用于较为简单的接口实现)。将这个行为提取到一个类中,就是适配器模式本身,只是原来的匿名实现类,变成了适配器类。
类适配器
适配器模式,都会将对于目标类/接口的调用,转化为对被适配类/接口的调用。
上文中,适配器类通过成员变量持有一个被适配对象。当适配器类被调用时,会最终委派给被适配对象的调用。这种通过持有一个被适配对象的适配器模式,称为对象适配器。另一种实现形式是类适配器。
类适配器通过继承来获得被适配类的功能。在继承后,类适配器中,可以直接调用被适配类的方法。
如果目标是一个类,那么类适配器就是目标类的子类,在代码中可以通过多态的形式,将适配器类传给目标类的引用。 如果目标是一个接口,那么类适配器需要通过实现的形式,称为目标接口的一个实现类。
因此区分对象适配器和类适配器的关键,在于适配器是通过何种形式获取被适配类的功能。如果是组合一个被适配类的对象,则是对象适配器,如果是通过继承被适配类,则是类适配器。
由于在设计模式中,通常认为组合优于继承,并且对于一些语言来说,例如java
,不支持多继承,无法同时继承被适配类和目标类。因此更多使用的是对象适配器。后续均以对象适配器代表适配器模式。
特点
用通俗的话来说,适配器模式就是,需要某个特定类/接口,但是提供的类和需要不符。通过新建一个适配器类,这个适配器类符合了需要,并在适配器类中完成提供的类和需要的类之间的转化。
适配器类可以理解为,把被适配类,当作目标类/接口的一种视角。
识别目标类和被适配类是关键的。很多文章中用到了插头和插座的例子。在这个例子中,不同国家的插座是被适配类,插头是目标类,插座不满足插头的要求。电源适配器完成了从不满足要求的插座,到满足要求的插座转化的过程。
适配器模式通常使用于,客户端和被适配类不兼容,且都无法修改的场景。
包装
适配器模式也被称为包装(Wrapper
)。广义的包装是指对一个已有对象,进行包裹,从而改变或扩展它们的行为,而无需修改原始对象的代码。典型的特征是用于包装的类通常通过成员变量持有被包装类的对象。就这个定义来说,后续会介绍的代理模式、桥接模式、装饰模式均属于包装。这四者在很多地方都很相似,难以区分,具体区别在每个模式文章中再单独阐述。
就适配器模式来说,他在包装中的特点是,被包装后的对象,和包装前的对象接口是不同的。
总结
适配器模式通常使用于,客户端和被适配类不兼容,且都无法修改的场景。
适配器模式中,通过适配器类,实现了目标类/接口,并通过组合的形式,将对于目标接口的调用,委派给被适配类,自身通常只负责接口或数据格式的转换。
客户端可以通过目标接口,使用适配器类。适配器类可以被视为被适配类在目标类上的切面。
适配器模式、代理模式、桥接模式、装饰模式均属于包装,特点是包装类中通过组合持有被包装类的对象。十分相似又有区别。