适配器模式

思考并回答以下问题:

  • 适配器模式和代理模式有什么区别?

本章导学

结构型模式关注如何将现有类或对象组织在一起形成更加强大的结构,在GOF设计模式中包含7种结构型设计模式,它们适用于不同的环境,使用不同的方式组合类与对象,使之可以协同工作。

适配器模式是一种使用频率非常高的结构型设计模式,如果在系统中存在不兼容的接口,可以通过引入一个适配器来使得原本因为接口不兼容而不能一起工作的两个类可以协同工作。

本章将对7种结构型模式进行简要的介绍,学习适配器模式的定义,掌握类适配器模式和对象适配器模式的结构与实现方式,并结合实例学习如何在实际软件项目开发中应用适配器模式,还将学习缺省适配器模式和双向适配器模式等适配器模式的扩展形式。

本章知识点

  • 适配器模式的定义
  • 适配器模式的结构
  • 适配器模式的实现
  • 适配器模式的应用
  • 适配器模式的优缺点
  • 适配器模式的适用环境
  • 缺省适配器模式
  • 双向适配器

结构型模式

在面向对象软件系统中,每个类/对象都承担了一定的职责,它们可以相互协作,实现一些复杂的功能。结构型模式(Structural Pattern)关注如何将现有类或对象组织在一起形成更加强大的结构。不同的结构型模式从不同的角度组合类或对象,它们在尽可能满足各种面向对象设计原则的同时为类或对象的组合提供一系列巧妙的解决方案。

结构型模式可以描述两种不同的东西:类与类的实例(即对象)。根据这一点,结构型模式可以分为类结构型模式和对象结构型模式。类结构型模式关心类的组合,由多个类组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系;而对象结构型模式关心类与对象的组合,通过关联关系,在一个类中定义另一个类的实例对象,然后通过该对象调用相应的方法。根据合成复用原则,在系统中尽量使用关联关系来替代继承关系,因此,大部分结构型模式都是对象结构型模式。

在GoF设计模式中包含7种结构型模式,它们的名称、定义、学习难度和使用频率如表1所示。

表1 结构型模式一览表

名 称
定 义
学习难度
使用频率
适配器模式
(Adapter Pattern)
将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。 ★★☆☆☆ ★★★★☆
桥接模式
(Bridge Pattern)
将抽象部分与它的实现部分解耦,使得两者都能够独立变化。 ★★★☆☆ ★★★☆☆
组合模式
(Composite Pattern)
组合多个对象形成树形结构,以表示具有部分-整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象。 ★★★☆☆ ★★★★☆
装饰模式
(Decorator Pattern)
动态地给一个对象增加一些额外的职责。就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案。 ★★★☆☆ ★★★☆☆
外观模式
(Facade Pattern)
为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 ★☆☆☆☆ ★★★★★
享元模式
(Flyweight Pattern)
运用共享技术有效地支持大量细粒度对象的复用。 ★★★★☆ ★☆☆☆☆
代理模式
(Proxy Pattern)
给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。 ★★★☆☆ ★★★★☆

适配器模式概述

众所周知,我们国家的生活用电的电压是220V,而笔记本电脑、手机等电子设备的工作电压没有这么高,为了使笔记本电脑、手机等设备可以使用220V的生活用电,需要电源适配器(AC Adapter),也就是充电器或变压器,有了这个电源适配器,生活用电和笔记本电脑就可以兼容了。在这里,电源适配器充当了一个适配器的角色,如图1所示。

图1 电源适配器示意图

在软件开发中,有时也存在类似这种不兼容的情况,也可以像引入一个电源适配器那样引入一个称之为适配器的角色来协调这些存在不兼容的结构,这种设计方案即为适配器。

与电源适配器相似,在适配器模式中引入了一个被称为适配器(Adapter)的包装类,而它所包装的对象称为适配者(Adaptee),即被适配的类。适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器让那些由于接口不兼容而不能交互的类可以一起工作。

适配器模式可以将一个类的接口和另一个类的接口匹配起来,而无须修改原来的适配者接口和抽象目标类接口。

适配器模式的定义如下:

1
将一个类的接口转换成客户希望的另一个接口适配器模式让那些接口不兼容的类可以一起工作。

适配器模式的别名为包装器(Wrapper)模式,它既可以作为类结构型模式,也可以作为对象结构型模式,在适配器模式定义中所提及的接口是指广义的接口,它可以表示一个方法或者方法的集合。

适配器模式的结构与实现

适配器模式包括类适配器和对象适配器。在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。下面分别对两种适配器进行结构分析。

适配器模式的结构

类适配器模式的结构如图2所示。

图2 类适配器模式结构图

对象适配器模式的结构如图3所示。

图3 对象适配器模式结构图

由图2和图3可知,适配器模式包含以下3个角色。

(1)Target(目标抽象类):目标抽象类定义客户所需的接口,可以是一个抽象类或接口,也可以是具体类。在类适配器中,由于C#语言不支持多重继承,它只能是接口。

(2)Adapter(适配器类):它可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配。适配器Adapter是适配器模式的核心,在类适配器中,它通过实现Target接口并继承Adaptee类来使二者产生联系,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。

(3)Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下甚至没有适配者类的源代码。

适配器模式的实现

由于适配器模式包括类适配器模式和对象适配器模式两种形式,下面分别介绍这两种适配器模式的实现机制。

1.类适配器

根据图2所示的类适配器模式结构图,在类适配器中,适配者类Adaptee没有Request()方法,而客户期待这个方法,但在适配者类中实现了SpecificRequest()方法,该方法所提供的实现正是客户所需要的。为了使客户能够使用适配者类,提供了一个中间类,即适配器类Adapter,适配器类实现了抽象目标类接口Target,并继承了适配者类,在适配器类的Request()方法中调用所继承的适配者类的SpecificRequest()方法,达到了适配的目的。因为适配器类与适配者类是继承关系,所以这种适配器模式称为类适配器模式。典型的类适配器代码如下:

1
2
3
4
5
6
7
class Adapter : Adaptee, Target
{
public void Request()
{
base.SpecificRequest();
}
}

2.对象适配器

根据图3所示的对象适配器模式结构图,在对象适配器中,客户端需要调用Request()方法,而适配者类Adaptee没有该方法,但是它所提供的SpecificRequest()方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提供一个包装类Adapter,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起来,在适配器的Request()方法中调用适配者的SpecificRequest()方法。因为适配器类与适配者类是关联关系(也可称为委派关系),所以这种适配器模式称为对象适配器模式。典型的对象适配器代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Adapter : Target
{
private Adaptee adaptee; // 维持一个对适配器对象的引用

public Adapter(Adaptee adaptee)
{
this.adaptee = adaptee;
}

public void Request()
{
adaptee.SpecificRequest(); // 转发调用
}
}

适配器模式可以将一个类的接口和另一个类的接口匹配起来,使用的前提是不能或不想修改原来的适配者接口和抽象目标类接口。例如,购买了一些第三方类库或控件,但是没有源代码,此时使用适配器模式可以统一对象访问接口。

适配器模式更多的是强调对代码的组织,而不是功能的实现。在实际开发中,对象适配器的使用频率更高。

适配器模式的应用实例

下面通过一个应用实例进一步学习和理解适配器模式。

1.实例说明

在为某学校开发教务管理系统时,开发人员发现需要对学生成绩进行排序和查找,该系统的设计人员已经开发了一个成绩操作接口ScoreOperation,在该接口中声明了排序方法Sort(int[])和查找方法Search(int[], int),为了提高排序和查找的效率,开发人员决定重用现有算法库中的快速排序算法类QuickSortClass和二分查找算法类BinarySearchClass,其中,QuickSortClass的QuickSort(int[])方法实现了快速排序,BinarySearchClass的BinarySearch(int[], int)方法实现了二分查找。

由于某些原因,开发人员已经找不到该算法库的源代码,无法直接通过复制和粘贴操作来重用其中的代码;而且部分开发人员已经针对ScoreOperation接口编程,如果再要求对该接口进行修改或要求大家直接使用QuickSortClass类和BinarySearchClass类将导致大量代码需要修改。

现使用适配器模式设计一个系统,在不修改已有代码的前提下将类QuickSortClass和类BinarySearchClass的相关方法适配到ScoreOperation接口中。

2.实例类图

通过分析,本实例采用对象适配器模式来实现,如图4所示。

图4 算法库重用结构图

在图4中,ScoreOperation接口充当抽象目标,QuickSortClass和BinarySearchClass类充当适配者,OperationAdapter充当适配器。

3.实例代码

(1)ScoreOperation:抽象成绩操作类,充当目标接口。

1
2
3
4
5
6
7
8
namespace AdapterSample
{
interface ScoreOperation
{
int[] Sort(int[] array); // 成绩排序
int Search(int[] array, int key); // 成绩查找
}
}

(2)QuickSortClass:快速排序类,充当适配者。

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
44
45
namespace AdapterSample
{
class QuickSortClass
{
public int[] QuickSort(int[] array)
{
Sort(array,0,array.Length - 1);
return array;
}

public void Sort(int[] array, int p, int r)
{
int q = 0;
if (p < r)
{
q = Partition(array,p,r);
Sort(array,p,q - 1);
Sort(array,q + 1,r);
}
}

public int Partition(int[] a, int p, int r)
{
int x = a[r];
int j = p - 1;
for (int i = p;i <= r - 1;i++)
{
if (a[i] <= x)
{
j++;
Swap(a,j,i);
}
}
Swap(a,j + 1,r);
return j + 1;
}

public void Swap(int[] a, int i, int j)
{
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
}

(3)BinarySearchClass:二分查找类,充当适配者。

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
namespace AdapterSample
{
class BinarySearchClass
{
public int BinarySearch(int[] array, int key)
{
int low = 0;
int high = array.Length -1;
while (low <= high)
{
int mid = (low + high) / 2;
int midVal = array[mid];
if (midVal < key)
{
low = mid +1;
}
else if (midVal > key)
{
high = mid -1;
}
else
{
return 1; // 找到元素返回1
}
}
return -1; // 未找到元素返回-1
}
}
}

(4)OperationAdapter:操作适配器,充当适配器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace AdapterSample
{
class OperationAdapter : ScoreOperation
{
private QuickSortClass sortObj; // 定义适配者QuickSortClass对象
private BinarySearchClass searchObj; // 定义适配者BinarySearchClass对象

public OperationAdapter()
{
sortObj = new QuickSortClass();
searchObj = new BinarySearchClass();
}

public int[] Sort(int[] array)
{
return sortObj.QuickSort(array); // 调用适配者类QuickSortClass的排序方法
}

public int Search(int[] array, int key)
{
return searchObj.BinarySearch(array, key); // 调用适配者类BinarySearchClass的查找方法
}
}
}

(5)配置文件App.config:在配置文件中存储了适配器类的类名。

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="adapter" value="AdapterSample.OperationAdapter"/>
</appSettings>
</configuration>

(6)Program:客户端测试类。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using System;
using System.Configuration;
using System.Reflection;

namespace AdapterSample
{
class Program
{
static void Main(string[] args)
{
ScoreOperation operation; // 针对抽象目标接口编程

// 读取配置文件
string adapterType = ConfigurationManager.AppSettings["adapter"];
// 反射生成对象
operation = (ScoreOperation)Assembly.Load("AdapterSample").CreateInstance(adapterType);

int[] scores = {84,76,50,69,90,91,88,96}; // 定义成绩数组
int[] result;
int score;

Console.WriteLine("成绩排序结果:");
result = operation.Sort(scores);

// 遍历输出成绩
foreach (int i in result)
{
Console.Write(i + ",");
}
Console.WriteLine();

Console.WriteLine("查找成绩90:");
score = operation.Search(result,90);
if (score != -1)
{
Console.WriteLine("找到成绩90。");
}
else
{
Console.WriteLine("没有找到成绩90。");
}

Console.WriteLine("查找成绩92:");
score = operation.Search(result,92);
if (score != -1)
{
Console.WriteLine("找到成绩92。");
}
else
{
Console.WriteLine("没有找到成绩92。");
}

Console.Read();
}
}
}

4.结果及分析

编译并运行程序,输出结果如下:

1
2
3
4
5
6
成绩排序结果:
50,69,76,84,88,90,91,96,
查找成绩 90:
找到成绩 90。
查找成绩 92:
没有找到成绩92。

在本实例中使用了对象适配器模式,同时引入了配置文件,将适配器类的类名存储在配置文件App.config中。如果需要使用其他排序算法类和查找算法类,可以增加一个新的适配器类,使用新的适配器来适配新的算法,原有代码无须修改。通过引入配置文件和反射机制,可以在不修改客户端代码的情况下使用新的适配器,无须修改源代码,符合开闭原则。

缺省适配器模式

缺省适配器模式是适配器模式的一种变体,其应用也较为广泛。

缺省适配器模式(Default Adapter Pattern)的定义如下:

1
当不需要实现一个妾口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中的每个方法提供一个默认实现(空方法),那么该抽象类的子类可以有选择性地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况,又称为单接口适配器模式。

缺省适配器模式的结构如图5所示。

图5 缺省适配器模式结构图

由图5可知,在缺省适配器模式中,包含以下3个角色。

(1)ServiceInterface(适配者接口):它是一个接口,通常在该接口中声明了大量的方法。

(2)AbstractServiceClass(缺省适配器):缺省适配器模式的核心类,使用空方法的形式实现了在Servicelnterface接口中声明的方法。通常将它定义为抽象类,因为对它进行实例化没有任何意义。

(3)ConcreteServiceClass(具体业务类):它是缺省适配器类的子类,在没有引入适配器之前,它需要实现适配者接口,因此需要实现在适配者接口中声明的所有方法,而对于一些无须使用的方法不得不提供空实现。在有了缺省适配器之后,可以直接继承该适配器类,根据需要有选择性地覆盖在适已器类中定义的方法。

其中,缺省适配器类的典型代码片段如下:

1
2
3
4
5
6
abstract class AbstractServiceClass : ServiceInterface
{
public void ServiceMethod1(){} // 空方法
public void ServiceMethod2(){} // 空方法
public void ServiceMethod3(){} // 空方法
}

双向适配器

在对象适配器的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,那么该适配器就是一个双向适配器,其结构示意图如图6所示。

图6 双向适配器模式示意图

双向适配器的实现较为复杂,其典型代码如下:

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
public class Adapter : Target, Adaptee
{
// 同时维持对抽象目标类和适配者的引用
private Target target;
private Adaptee adaptee;

public Adapter(Target target)
{
this.target = target;
}

public Adapter(Adaptee adaptee)
{
this.adaptee = adaptee;
}

public void Request()
{
adaptee.SpecificRequest();
}

public void SpecificRequest()
{
target.Request();
}
}

适配器模式的优点

无论是对象适配器模式还是类适配器模式都具有以下优点:

  • (1)将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
  • (2)增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
  • (3)灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合开闭原则。

具体来说,类适配器模式还具有以下优点:

由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。

对象适配器模式还具有以下优点:

  • (1)一个对象适配器可以把多个不同的适配者适配到同一个目标。
  • (2)对象适配器模式可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据里氏代换原则,适配者的子类也可通过该适配器进行适配。

适配器模式的缺点

类适配器模式的主要缺点如下:

  • (1)对于C#Java等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者。
  • (2)适配者类不能为最终类,例如在C#中不能为sealed类。
  • (3)在C#Java等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。

对象适配器模式的主要缺点如下:

与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把该适配者类的子类当作真正的适配者进行适配,其实现过程较为复杂。

适配器模式的适用环境

在以下情况下可以考虑使用适配器模式:

  • (1)系统需要使用一些现有的类,而这些类的接口(例如方法名)不符合系统的需要,甚至没有这些类的源代码。
  • (2)创建一个可以重复使用的类,用于和一些彼此之间没有太大关联的类,包括一些可能在将来引进的类一起工作。

本章小结

(1)结构型模式关注如何将现有类或对象组设计模式中一共包含7种结构型模式。

(2)适配器模式将一个类的接口转换成客户希望的另一个接口,适配器模式让那些接口不兼容的类可以一起工作。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

(3)适配器模式包含目标抽象类、适配器类和适配者类3个角色。其中,目标抽象类定义客户所需的接口,可以是一个抽象类或接口,也可以是具体类;适配器类可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配;适配者类即被适配的角色

(4)适配器模式的主要优点是将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构;增加了类的透明性和复用性且让系统的灵活性和展性都非常好,此外,在类适配器模式中置换一些适配者的方法很方便;通过对象适配器模式可以把多个不同的适配者适配到同一个目标,还可以适配一个适配者的子类。类适配器模式的主要缺点是一次最多只能适配一个适配者类,不能同时适配多个适配者;适配者类不能为最终类,且类适配器模式中的目标抽象类只能为接口,不能为类。对象适配器模式的主要缺点是在适配器中置换适配者类的某些方法比较麻烦。

(5)适配器模式适用的环境:系统需要使用一些现有的类,而这些类的接口不符合系统的需要,甚至没有这些类的源代码;想创建一个可以重复使用的类,用于和一些彼此之间没有太大关联的类,包括一些可能在将来引进的类一起工作。

(6)缺省适配器模式是指当不需要实现一个接口所提供的所有方法时,可先设计一个日象类实现该接口,并为接口中的每个方法提供一个默认实现(空方法),那么该抽象类的子 类可以有选择性地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况。

(7)在使用对象适配器的过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,该适配器就是一个双向适配器。

0%