【學習難度:★★☆☆☆,使用頻率:★★★★☆】
直接出處:適配器模式
梳理和學習:https://github.com/BruceOuyang/boy-design-pattern
簡書日期: 2018/03/09
簡書首頁:http://www.itdecent.cn/p/0fb891a7c5ed
不兼容結構的協(xié)調——適配器模式(一)
我的筆記本電腦的工作電壓是20V,而我國的家庭用電是220V,如何讓20V的筆記本電腦能夠在220V的電壓下工作?答案是引入一個電源適配器(AC Adapter),俗稱充電器或變壓器,有了這個電源適配器,生活用電和筆記本電腦即可兼容,如圖9-1所示:

在軟件開發(fā)中,有時也存在類似這種不兼容的情況,我們也可以像引入一個電源適配器一樣引入一個稱之為適配器的角色來協(xié)調這些存在不兼容的結構,這種設計方案即為適配器模式。
9.1 沒有源碼的算法庫
Sunny軟件公司在很久以前曾開發(fā)了一個算法庫,里面包含了一些常用的算法,例如排序算法和查找算法,在進行各類軟件開發(fā)時經常需要重用該算法庫中的算法。在為某學校開發(fā)教務管理系統(tǒng)時,開發(fā)人員發(fā)現需要對學生成績進行排序和查找,該系統(tǒng)的設計人員已經開發(fā)了一個成績操作接口ScoreOperation,在該接口中聲明了排序方法sort(int[]) 和查找方法search(int[], int),為了提高排序和查找的效率,開發(fā)人員決定重用算法庫中的快速排序算法類QuickSort和二分查找算法類BinarySearch,其中QuickSort的quickSort(int[])方法實現了快速排序,BinarySearch 的binarySearch (int[], int)方法實現了二分查找。
由于某些原因,現在Sunny公司開發(fā)人員已經找不到該算法庫的源代碼,無法直接通過復制和粘貼操作來重用其中的代碼;部分開發(fā)人員已經針對ScoreOperation接口編程,如果再要求對該接口進行修改或要求大家直接使用QuickSort類和BinarySearch類將導致大量代碼需要修改。
Sunny軟件公司開發(fā)人員面對這個沒有源碼的算法庫,遇到一個幸福而又煩惱的問題:如何在既不修改現有接口又不需要任何算法庫代碼的基礎上能夠實現算法庫的重用?
通過分析,我們不難得知,現在Sunny軟件公司面對的問題有點類似本章最開始所提到的電壓問題,成績操作接口ScoreOperation好比只支持20V電壓的筆記本,而算法庫好比220V的家庭用電,這兩部分都沒有辦法再進行修改,而且它們原本是兩個完全不相關的結構,如圖9-2所示:

現在我們需要ScoreOperation接口能夠和已有算法庫一起工作,讓它們在同一個系統(tǒng)中能夠兼容,最好的實現方法是增加一個類似電源適配器一樣的適配器角色,通過適配器來協(xié)調這兩個原本不兼容的結構。如何在軟件開發(fā)中設計和實現適配器是本章我們將要解決的核心問題,下面就讓我們正式開始學習這種用于解決不兼容結構問題的適配器模式。
9.2 適配器模式概述
與電源適配器相似,在適配器模式中引入了一個被稱為適配器(Adapter)的包裝類,而它所包裝的對象稱為適配者(Adaptee),即被適配的類。適配器的實現就是把客戶類的請求轉化為對適配者的相應接口的調用。也就是說:當客戶類調用適配器的方法時,在適配器類的內部將調用適配者類的方法,而這個過程對客戶類是透明的,客戶類并不直接訪問適配者類。因此,適配器讓那些由于接口不兼容而不能交互的類可以一起工作。
適配器模式可以將一個類的接口和另一個類的接口匹配起來,而無須修改原來的適配者接口和抽象目標類接口。適配器模式定義如下:
適配器模式(Adapter Pattern):將一個接口轉換成客戶希望的另一個接口,使接口不兼容的那些類可以一起工作,其別名為包裝器(Wrapper)。適配器模式既可以作為類結構型模式,也可以作為對象結構型模式。
【注:在適配器模式定義中所提及的接口是指廣義的接口,它可以表示一個方法或者方法的集合?!?/p>
在適配器模式中,我們通過增加一個新的適配器類來解決接口不兼容的問題,使得原本沒有任何關系的類可以協(xié)同工作。根據適配器類與適配者類的關系不同,適配器模式可分為對象適配器和類適配器兩種,在對象適配器模式中,適配器與適配者之間是關聯關系;在類適配器模式中,適配器與適配者之間是繼承(或實現)關系。在實際開發(fā)中,對象適配器的使用頻率更高,對象適配器模式結構如圖9-3所示:

在對象適配器模式結構圖中包含如下幾個角色:
Target(目標抽象類):目標抽象類定義客戶所需接口,可以是一個抽象類或接口,也可以是具體類。
Adapter(適配器類):適配器可以調用另一個接口,作為一個轉換器,對Adaptee和Target進行適配,適配器類是適配器模式的核心,在對象適配器中,它通過繼承Target并關聯一個Adaptee對象使二者產生聯系。
Adaptee(適配者類):適配者即被適配的角色,它定義了一個已經存在的接口,這個接口需要適配,適配者類一般是一個具體類,包含了客戶希望使用的業(yè)務方法,在某些情況下可能沒有適配者類的源代碼。
根據對象適配器模式結構圖,在對象適配器中,客戶端需要調用request()方法,而適配者類Adaptee沒有該方法,但是它所提供的specificRequest()方法卻是客戶端所需要的。為了使客戶端能夠使用適配者類,需要提供一個包裝類Adapter,即適配器類。這個包裝類包裝了一個適配者的實例,從而將客戶端與適配者銜接起來,在適配器的request()方法中調用適配者的specificRequest()方法。因為適配器類與適配者類是關聯關系(也可稱之為委派關系),所以這種適配器模式稱為對象適配器模式。典型的對象適配器代碼如下所示:
class Adapter extends Target {
private Adaptee adaptee; //維持一個對適配者對象的引用
public Adapter(Adaptee adaptee) {
this.adaptee=adaptee;
}
public void request() {
adaptee.specificRequest(); //轉發(fā)調用
}
}
思考
在對象適配器中,一個適配器能否適配多個適配者?如果能,應該如何實現?如果不能,請說明原因?
不兼容結構的協(xié)調——適配器模式(二)
9.3 完整解決方案
Sunny軟件公司開發(fā)人員決定使用適配器模式來重用算法庫中的算法,其基本結構如圖9-4所示:

在圖9-4中,ScoreOperation接口充當抽象目標,QuickSort和BinarySearch類充當適配者,OperationAdapter充當適配器。完整代碼如下所示:
//抽象成績操作類:目標接口
interface ScoreOperation {
public int[] sort(int array[]); //成績排序
public int search(int array[],int key); //成績查找
}
//快速排序類:適配者
class QuickSort {
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;
}
}
//二分查找類:適配者
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 1; //找到元素返回1
}
}
return -1; //未找到元素返回-1
}
}
//操作適配器:適配器
class OperationAdapter implements ScoreOperation {
private QuickSort sortObj; //定義適配者QuickSort對象
private BinarySearch searchObj; //定義適配者BinarySearch對象
public OperationAdapter() {
sortObj = new QuickSort();
searchObj = new BinarySearch();
}
public int[] sort(int array[]) {
return sortObj.quickSort(array); //調用適配者類QuickSort的排序方法
}
public int search(int array[],int key) {
return searchObj.binarySearch(array,key); //調用適配者類BinarySearch的查找方法
}
}
為了讓系統(tǒng)具備良好的靈活性和可擴展性,我們引入了工具類XMLUtil和配置文件,其中,XMLUtil類的代碼如下所示:
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
class XMLUtil {
//該方法用于從XML配置文件中提取具體類類名,并返回一個實例對象
public static Object getBean() {
try {
//創(chuàng)建文檔對象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml"));
//獲取包含類名的文本節(jié)點
NodeList nl = doc.getElementsByTagName("className");
Node classNode=nl.item(0).getFirstChild();
String cName=classNode.getNodeValue();
//通過類名生成實例對象并將其返回
Class c=Class.forName(cName);
Object obj=c.newInstance();
return obj;
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
}
配置文件config.xml中存儲了適配器類的類名,代碼如下所示:
<?xml version="1.0"?>
<config>
<className>OperationAdapter</className>
</config>
編寫如下客戶端測試代碼:
class Client {
public static void main(String args[]) {
ScoreOperation operation; //針對抽象目標接口編程
operation = (ScoreOperation)XMLUtil.getBean(); //讀取配置文件,反射生成對象
int scores[] = {84,76,50,69,90,91,88,96}; //定義成績數組
int result[];
int score;
System.out.println("成績排序結果:");
result = operation.sort(scores);
//遍歷輸出成績
for(int i : scores) {
System.out.print(i + ",");
}
System.out.println();
System.out.println("查找成績90:");
score = operation.search(result,90);
if (score != -1) {
System.out.println("找到成績90。");
}
else {
System.out.println("沒有找到成績90。");
}
System.out.println("查找成績92:");
score = operation.search(result,92);
if (score != -1) {
System.out.println("找到成績92。");
}
else {
System.out.println("沒有找到成績92。");
}
}
}
編譯并運行程序,輸出結果如下:
成績排序結果:
50,69,76,84,88,90,91,96,
查找成績90:
找到成績90。
查找成績92:
沒有找到成績92。
在本實例中使用了對象適配器模式,同時引入了配置文件,將適配器類的類名存儲在配置文件中。如果需要使用其他排序算法類和查找算法類,可以增加一個新的適配器類,使用新的適配器來適配新的算法,原有代碼無須修改。通過引入配置文件和反射機制,可以在不修改客戶端代碼的情況下使用新的適配器,無須修改源代碼,符合“開閉原則”。
不兼容結構的協(xié)調——適配器模式(三)
9.4 類適配器
除了對象適配器模式之外,適配器模式還有一種形式,那就是類適配器模式,類適配器模式和對象適配器模式最大的區(qū)別在于適配器和適配者之間的關系不同,對象適配器模式中適配器和適配者之間是關聯關系,而類適配器模式中適配器和適配者是繼承關系,類適配器模式結構如圖9-5所示:

根據類適配器模式結構圖,適配器類實現了抽象目標類接口Target,并繼承了適配者類,在適配器類的request()方法中調用所繼承的適配者類的specificRequest()方法,實現了適配。
典型的類適配器代碼如下所示:
class Adapter extends Adaptee implements Target {
public void request() {
specificRequest();
}
}
由于Java、C#等語言不支持多重類繼承,因此類適配器的使用受到很多限制,例如如果目標抽象類Target不是接口,而是一個類,就無法使用類適配器;此外,如果適配者Adapter為最終(Final)類,也無法使用類適配器。在Java等面向對象編程語言中,大部分情況下我們使用的是對象適配器,類適配器較少使用。
思考
在類適配器中,一個適配器能否適配多個適配者?如果能,應該如何實現?如果不能,請說明原因?
9.5 雙向適配器
在對象適配器的使用過程中,如果在適配器中同時包含對目標類和適配者類的引用,適配者可以通過它調用目標類中的方法,目標類也可以通過它調用適配者類中的方法,那么該適配器就是一個雙向適配器,其結構示意圖如圖9-6所示:

雙向適配器的實現較為復雜,其典型代碼如下所示:
class Adapter implements 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();
}
}
在實際開發(fā)中,我們很少使用雙向適配器。
不兼容結構的協(xié)調——適配器模式(四)
9.6 缺省適配器
缺省適配器模式是適配器模式的一種變體,其應用也較為廣泛。缺省適配器模式的定義如下:
缺省適配器模式(Default Adapter Pattern):當不需要實現一個接口所提供的所有方法時,可先設計一個抽象類實現該接口,并為接口中每個方法提供一個默認實現(空方法),那么該抽象類的子類可以選擇性地覆蓋父類的某些方法來實現需求,它適用于不想使用一個接口中的所有方法的情況,又稱為單接口適配器模式。
缺省適配器模式結構如圖9-7所示:

在缺省適配器模式中,包含如下三個角色:
ServiceInterface(適配者接口):它是一個接口,通常在該接口中聲明了大量的方法。
AbstractServiceClass(缺省適配器類):它是缺省適配器模式的核心類,使用空方法的形式實現了在ServiceInterface接口中聲明的方法。通常將它定義為抽象類,因為對它進行實例化沒有任何意義。
ConcreteServiceClass(具體業(yè)務類):它是缺省適配器類的子類,在沒有引入適配器之前,它需要實現適配者接口,因此需要實現在適配者接口中定義的所有方法,而對于一些無須使用的方法也不得不提供空實現。在有了缺省適配器之后,可以直接繼承該適配器類,根據需要有選擇性地覆蓋在適配器類中定義的方法。
在JDK類庫的事件處理包java.awt.event中廣泛使用了缺省適配器模式,如WindowAdapter、KeyAdapter、MouseAdapter等。下面我們以處理窗口事件為例來進行說明:在Java語言中,一般我們可以使用兩種方式來實現窗口事件處理類,一種是通過實現WindowListener接口,另一種是通過繼承WindowAdapter適配器類。如果是使用第一種方式,直接實現WindowListener接口,事件處理類需要實現在該接口中定義的七個方法,而對于大部分需求可能只需要實現一兩個方法,其他方法都無須實現,但由于語言特性我們不得不為其他方法也提供一個簡單的實現(通常是空實現),這給使用帶來了麻煩。而使用缺省適配器模式就可以很好地解決這一問題,在JDK中提供了一個適配器類WindowAdapter來實現WindowListener接口,該適配器類為接口中的每一個方法都提供了一個空實現,此時事件處理類可以繼承WindowAdapter類,而無須再為接口中的每個方法都提供實現。如圖9-8所示:

9.7 適配器模式總結
適配器模式將現有接口轉化為客戶類所期望的接口,實現了對現有類的復用,它是一種使用頻率非常高的設計模式,在軟件開發(fā)中得以廣泛應用,在Spring等開源框架、驅動程序設計(如JDBC中的數據庫驅動程序)中也使用了適配器模式。
- 主要優(yōu)點
無論是對象適配器模式還是類適配器模式都具有如下優(yōu)點:
(1) 將目標類和適配者類解耦,通過引入一個適配器類來重用現有的適配者類,無須修改原有結構。
(2) 增加了類的透明性和復用性,將具體的業(yè)務實現過程封裝在適配者類中,對于客戶端類而言是透明的,而且提高了適配者的復用性,同一個適配者類可以在多個不同的系統(tǒng)中復用。
(3) 靈活性和擴展性都非常好,通過使用配置文件,可以很方便地更換適配器,也可以在不修改原有代碼的基礎上增加新的適配器類,完全符合“開閉原則”。
具體來說,類適配器模式還有如下優(yōu)點:
由于適配器類是適配者類的子類,因此可以在適配器類中置換一些適配者的方法,使得適配器的靈活性更強。
對象適配器模式還有如下優(yōu)點:
(1) 一個對象適配器可以把多個不同的適配者適配到同一個目標;
(2) 可以適配一個適配者的子類,由于適配器和適配者之間是關聯關系,根據“里氏代換原則”,適配者的子類也可通過該適配器進行適配。
- 主要缺點
類適配器模式的缺點如下:
(1) 對于Java、C#等不支持多重類繼承的語言,一次最多只能適配一個適配者類,不能同時適配多個適配者;
(2) 適配者類不能為最終類,如在Java中不能為final類,C#中不能為sealed類;
(3) 在Java、C#等語言中,類適配器模式中的目標抽象類只能為接口,不能為類,其使用有一定的局限性。
對象適配器模式的缺點如下:
與類適配器模式相比,要在適配器中置換適配者類的某些方法比較麻煩。如果一定要置換掉適配者類的一個或多個方法,可以先做一個適配者類的子類,將適配者類的方法置換掉,然后再把適配者類的子類當做真正的適配者進行適配,實現過程較為復雜。
- 適用場景
在以下情況下可以考慮使用適配器模式:
(1) 系統(tǒng)需要使用一些現有的類,而這些類的接口(如方法名)不符合系統(tǒng)的需要,甚至沒有這些類的源代碼。
(2) 想創(chuàng)建一個可以重復使用的類,用于與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起工作。
練習
Sunny軟件公司OA系統(tǒng)需要提供一個加密模塊,將用戶機密信息(如口令、郵箱等)加密之后再存儲在數據庫中,系統(tǒng)已經定義好了數據庫操作類。為了提高開發(fā)效率,現需要重用已有的加密算法,這些算法封裝在一些由第三方提供的類中,有些甚至沒有源代碼。試使用適配器模式設計該加密模塊,實現在不修改現有類的基礎上重用第三方加密方法。
練習會在我的github上做掉