一口一口啃完Java中的24种设计模式---适配器模式



>

问题引入

日常使用的笔记本的工作电压为 20v ,但是我国家庭用电的电压为 220V ,为了让笔记本电脑可以在国内所有的插座上充电使用,这里就会引入一个电源适配器,如图:

电源适配器

有了这个电源适配器之后,生活用电和笔记本电脑用电的电压就可以兼容。这里体现出来的设计模式就是 适配器模式

对象适配器模式概述

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

【注:在适配器模式定义中所提及的 接口 是指广义的接口,它可以表示一个方法或者方法的集合。】

在适配器模式中,我们通过新增一个适配器类来解决接口不兼容的问题,使原本没有任何关系的类可以协同使用。

适配器模式有两种实现方式,这里先说其中一种,对象适配器模式,UML 图:

在对象适配器模式结构图中包含如下几个角色:

  • Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。

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

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

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

没有源码的算法库的适配器解决方案

问题描述

背景:某公司之前开发过一个算法库,其中包括常用的算法,例如查找算法、排序算法等,在进行各类软件开发时经常会用到这个算法库。在为某学校开发教务管理系统时,开发人员发现需要对学生成绩进行排序和查找,该系统的设计人员已经开发了一个成绩操作接口 ScoreOperation,在该接口中声明了排序方法 sort(int[]) 和查找方法 search(int[], int),为了提高排序和查找的效率,开发人员决定重用算法库中的快速排序算法类 QuickSort 和二分查找算法类 BinarySearch,其中 QuickSort 的 quickSort(int[]) 方法实现了快速排序,BinarySearch 的 binarySearch (int[], int) 方法实现了二分查找。

但由于某些原因,开发人员现在找不到该算法库的源码,不能通过复制粘贴的方式去实现 ScoreOperation 中的方法,并且部分开发人员已经针对 ScoreOperation 的接口进行了编程,如果再要求对该接口进行修改或要求大家直接使用 QuickSort 类和 BinarySearch 类将导致大量代码需要修改。

现在的问题就是:如何在既不修改现有接口又不需要任何算法库代码的基础上能够实现算法库的重用?

因为 ScoreOperation 接口已经确定,并且基于该接口已经进行了后续的开发了,所以这个接口肯定是不能修改的,而算法库已经引用到各类软件中,修改算法库的代价太大也没有必要,这是就要可以引入一个适配器 Adapter, 将算法库的接口适配成 ScoreOperation 的接口。

UML 图

结合上述对适配器模式的讲解,这个问题很符合适配器模式可以解决范围,UML 结构图如下:

编码实现

target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 成绩操作接口
*
* @author: fanyuzeng on 2018/2/24 11:14
*/
public interface ScoreOperation {
/**
* 对成绩进行排序
*
* @param array 成绩数组
* @return 排序过后的成绩数组
*/
int[] sort(int[] array);
/**
* 查找某一特定成绩
*
* @param array 成绩数组
* @param key 特定成绩
* @return 若成绩数据中存在该特定成绩,则返回该特定成绩在成绩数组中的索引,否则返回 -1
*/
int search(int[] array, int key);
}

adaptee

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
/**
* 快速排序工具类
*
* @author: fanyuzeng on 2018/2/24 11:18
*/
public class QuickSort {
public int[] quickSort(int array[]) {
sort(array, 0, array.length - 1);
return array;
}
private 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);
}
}
private 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;
}
private void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 二分查找工具类
*
* @author: fanyuzeng on 2018/2/24 11:18
*/
public class BinarySearch {
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 mid; //找到元素返回索引
}
}
return -1; //未找到元素返回-1
}
}

adapter

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
/**
* @author: fanyuzeng on 2018/2/24 11:22
*/
public class OperationAdapter implements ScoreOperation {
//适配者
private BinarySearch mBinarySearch;
//适配者
private QuickSort mQuickSort;
public OperationAdapter() {
mBinarySearch = new BinarySearch();
mQuickSort = new QuickSort();
}
@Override
public int[] sort(int[] array) {
//调用适配者的快排方法
return mQuickSort.quickSort(array);
}
@Override
public int search(int[] array, int key) {
//调用适配者的二分查找方法
return mBinarySearch.binarySearch(array, key);
}
}

client

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
/**
* @author: fanyuzeng on 2018/2/24 11:23
*/
public class Test {
public static void main(String[] args) {
ScoreOperation operation; //针对抽象目标接口编程
operation = new OperationAdapter();
int scores[] = {84, 76, 50, 69, 90, 91, 88, 96}; //定义成绩数组
int result[];
int index;
System.out.println("成绩排序结果:");
result = operation.sort(scores);
//遍历输出成绩
for (int i : scores) {
System.out.print(i + ",");
}
System.out.println();
System.out.println("查找成绩90:");
index = operation.search(result, 90);
if (index != -1) {
System.out.println("找到成绩90。在数组中的所因为:" + index);
} else {
System.out.println("没有找到成绩90。");
}
System.out.println("查找成绩92:");
index = operation.search(result, 92);
if (index != -1) {
System.out.println("找到成绩92。在数组中的所因为:" + index);
} else {
System.out.println("没有找到成绩92。");
}
}
}

输出为:

成绩排序结果:
50,69,76,84,88,90,91,96,
查找成绩90:
找到成绩90。
查找成绩92:
没有找到成绩92。

类适配器模式

除了对象适配器模式之外,适配器模式还有一种形式,那就是 类适配器模式,类适配器模式和对象适配器模式最大的区别在于适配器和适配者之间的关系不同,对象适配器模式中适配器和适配者之间是关联关系,而类适配器模式中适配器和适配者是继承关系,类适配器模式结构如图所示:

典型代码:

1
2
3
4
5
6
7
calss Adapter imlements Target extends Adaptee{
@override
public void request(){
specificRequest();
}
}

由于 Java、C# 等语言不支持多重类继承,因此类适配器的使用受到很多限制,例如如果目标抽象类 Target 不是接口,而是一个类,就无法使用类适配器;此外,如果适配者 Adapter 为最终(Final)类,也无法使用类适配器。在 Java 等面向对象编程语言中,大部分情况下我们使用的是对象适配器,类适配器较少使用

缺省适配器

缺省适配器模式是适配器模式的一种变体,其应用也较为广泛。缺省适配器模式的定义如下: 缺省适配器模式(Default Adapter Pattern):当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可以选择性地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况,又称为单接口适配器模式。UML 图如下:

在缺省适配器模式中,包含如下三个角色:

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

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

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

在 JDK 类库的事件处理包 java.awt.event 中广泛使用了缺省适配器模式,如 WindowAdapter、KeyAdapter、MouseAdapter 等。下面我们以处理窗口事件为例来进行说明:在 Java 语言中,一般我们可以使用两种方式来实现窗口事件处理类,一种是通过实现 WindowListener 接口,另一种是通过继承 WindowAdapter 适配器类

如果是使用第一种方式,直接实现 WindowListener 接口,事件处理类需要实现在该接口中定义的七个方法,而对于大部分需求可能只需要实现一两个方法,其他方法都无须实现,但由于语言特性我们不得不为其他方法也提供一个简单的实现(通常是空实现),这给使用带来了麻烦。

而使用缺省适配器模式就可以很好地解决这一问题,在 JDK 中提供了一个适配器类 WindowAdapter 来实现 WindowListener 接口,该适配器类为接口中的每一个方法都提供了一个空实现,此时事件处理类可以继承 WindowAdapter 类,而无须再为接口中的每个方法都提供实现。

适配器模式总结

主要优点

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

(1) 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。

(2) 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。

(3) 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

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

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

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

(1) 一个对象适配器可以把多个不同的适配者适配到同一个目标;

(2) 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配。

主要缺点

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

(1) 对于 Java、C# 等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者;

(2) 适配者类不能为最终类,如在 Java 中不能为 final 类,C# 中不能为 sealed 类;

(3) 在 Java、C# 等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。

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

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

适用场景

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

(1) 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。

(2) 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

共82.3k字
0%
.gt-container a{border-bottom: none;}