說明:設計模式系列文章是讀劉偉
所著《設計模式的藝術之道(軟體開發人員內功修煉之道)》
一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的部落格https://blog.csdn.net/LoveLion/article/details/17517213
。
模式概述
模式定義
與電源介面卡相似,在介面卡模式中引入了一個被稱為介面卡(Adapter
)的包裝類,而它所包裝的物件稱為適配者(Adaptee
),即被適配的類。介面卡的實現就是把客戶類的請求轉化為對適配者的相應介面的呼叫。也就是說:當客戶類呼叫介面卡的方法時,在介面卡類的內部將呼叫適配者類的方法,而這個過程對客戶類是透明的,客戶類並不直接訪問適配者類。因此,介面卡讓那些由於介面不相容而不能互動的類可以一起工作。
介面卡模式(
Adapter Pattern
): 將一個介面轉換成期望的另一個介面,使介面不相容的那些類可以一起工作,其別名為包裝器(Wrapper
)。介面卡模式既可以作為類結構型模式,也可以作為物件結構型模式。
注意:在介面卡模式定義中所提及的介面是指廣義的介面,它可以表示一個方法或者方法的集合。
模式結構圖
在介面卡模式中,我們通過增加一個新的介面卡類來解決介面不相容的問題,使得原本沒有任何關係的類可以協同工作。根據介面卡類與適配者類的關係不同,介面卡模式可分為物件介面卡
和類介面卡
兩種,在物件介面卡
模式中,介面卡與適配者之間是關聯關係
;在類介面卡
模式中,介面卡與適配者之間是繼承
關係。在實際開發中,物件介面卡的使用頻率更高,物件介面卡模式結構如圖所示
在物件介面卡模式結構圖中包含如下幾個角色:
Target(目標抽象類)
:目標抽象類定義客戶所需介面,可以是一個抽象類或介面,也可以是具體類。Adapter(介面卡類)
:介面卡可以呼叫另一個介面,作為一個轉換器,對Adaptee
和Target
進行適配,介面卡類是介面卡模式的核心,在物件介面卡中,它通過繼承(或者實現)Target
並關聯一個Adaptee
物件使二者產生聯絡。Adaptee(適配者類)
:適配者即被適配的角色,它定義了一個已經存在的介面,這個介面需要適配,適配者類一般是一個具體類,包含了客戶希望使用的業務方法,在某些情況下可能沒有適配者類的原始碼。
模式虛擬碼
在物件介面卡
中,客戶端需要呼叫request()
方法,而適配者類Adaptee
沒有該方法,但是它所提供的specificRequest()
方法卻是客戶端所需要的。為了使客戶端能夠使用適配者類,需要提供一個包裝類Adapter
,即介面卡類。這個包裝類包裝了一個適配者的例項,從而將客戶端與適配者銜接起來,在介面卡的request()
方法中呼叫適配者的specificRequest()
方法。因為介面卡類與適配者類是關聯關係
(也可稱之為委派關係),所以這種介面卡模式稱為物件介面卡模式
。典型的物件介面卡程式碼如下所示:
class Adapter implements Target {
// 維持一個對適配者物件的引用
private Adaptee adaptee;
// 構造注入適配者
public Adapter(Adaptee adaptee) {
this.adaptee=adaptee;
}
@Override
public void request() {
// 轉發呼叫
adaptee.specificRequest();
}
}
類介面卡,雙向介面卡,預設介面卡
類介面卡
類介面卡
模式和物件介面卡
模式最大的區別在於介面卡和適配者之間的關係不同,物件介面卡模式中介面卡和適配者之間是關聯關係
,而類介面卡模式中介面卡和適配者是繼承關係
介面卡類實現了抽象目標類介面Target
,並繼承了適配者類,在介面卡類的request()
方法中呼叫所繼承的適配者類的specificRequest()
方法,實現了適配。
典型程式碼實現如下:
class Adapter extends Adaptee implements Target {
@Override
public void request() {
specificRequest();
}
}
由於Java
、C#
等語言不支援多重類繼承,因此類介面卡
的使用受到很多限制,例如如果目標抽象類Target
不是介面,而是一個類,就無法使用類介面卡;此外,如果適配者Adaptee
為最終(final
)類,也無法使用類介面卡。在Java等物件導向程式語言中,大部分情況下我們使用的是物件介面卡,類介面卡
較少使用。
雙向介面卡
雙向介面卡
: 在物件介面卡
的使用過程中,如果在介面卡中同時包含對目標類和適配者類的引用,適配者可以通過它呼叫目標類中的方法,目標類也可以通過它呼叫適配者類中的方法,那麼該介面卡就是一個雙向介面卡。
典型程式碼實現如下:
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;
}
@Override
public void request() {
adaptee.specificRequest();
}
@Override
public void specificRequest() {
target.request();
}
}
在實際開發中,我們很少使用雙向介面卡
。違背了單一職責
原則,相當於一個介面卡承擔了兩個介面卡的職責。
預設介面卡
預設介面卡
模式是介面卡模式的一種變體,其應用也較為廣泛。
預設介面卡模式(Default Adapter Pattern)
:當不需要實現一個介面所提供的所有方法時,可先設計一個抽象類實現該介面,併為介面中每個方法提供一個預設實現(空方法),那麼該抽象類的子類可以選擇性地覆蓋父類的某些方法來實現需求,它適用於不想使用一個介面中的所有方法的情況,又稱為單介面介面卡模式
。
典型程式碼實現如下:
abstract class Adapter implements Target {
@Override
public void request1() {
// 空實現,讓具體實現類去有選擇地實現
}
@Override
public void request2() {
// 空實現,讓具體實現類去有選擇地實現
}
@Override
public void request3() {
// 空實現,讓具體實現類去有選擇地實現
}
}
class ConcreteAdapter extends Adapter {
// 維持一個對適配者物件的引用
private Adaptee adaptee;
// 構造注入適配者
public Adapter(Adaptee adaptee) {
this.adaptee=adaptee;
}
@Override
public void request1() {
// 只實現request1
adaptee.specificRequest();
}
}
模式應用
模式在JDK中的應用
在JDK中,IO
類中也大量使用到了介面卡模式。比如說StringReader
將String
適配到Reader
,InputStreamReader
將InputStream
適配到Reader
等等。
這裡用StringReader
來說明。這裡的StringReader
相當於上述的Adapter
,Reader
相當於上述的Target
,String
相當於上述的Adaptee
public class StringReader extends Reader {
// 維持對adaptee物件的引用
private String str;
private int length;
private int next = 0;
private int mark = 0;
/**
* 構造注入一個String用於之後的read操作
*/
public StringReader(String s) {
this.str = s;
this.length = s.length();
}
// 這裡相當於是在做適配操作,轉為目標物件所期望的請求
public int read() throws IOException {
synchronized (lock) {
ensureOpen();
if (next >= length)
return -1;
return str.charAt(next++);
}
}
}
模式在開源專案中的應用
其實不只是開源專案,我們自己寫的專案很多地方都是隱含著介面卡模式,只是有時候這種特性表現的不是很明顯(因為我們很自然去使用),以至於我們都沒有給類名命成XxxAdapter
,比如說我們使用第三方庫,第三方庫某方法名太長或者引數過多,或者呼叫過於複雜了,我們可能會對第三方庫再次做個封裝,把適合自己專案當前業務邏輯的預設引數,預設實現補充完整,讓其他地方很方便呼叫,舉個具體例子,專案中可能經常會用到HttpClient
,大多數情況下,對現有的HttpClient
再次封裝(比如client的建立、http響應結果的統一處理等等),封裝成方便自己專案使用的SpecialHttpClient
,如果你還想切換不同的底層HttpClient
實現,還可以對SpecialHttpClient
抽出來一個介面,通過不同的Adapter
來注入不同的HttpClient
(比如apache的HttpClient
、OkHttpClient
、Spring的RestTemplate
以及WebClient
等等)來實現,這種很自然的思想 個人覺得本質上也用到了介面卡模式
,相當於是把第三方的HttpClient
適配成了自己的SpecialHttpClient
。
當轉換的源不是單一的時候,這種介面卡
思想就凸顯出來了(對應上面的例子就是說 專案中需要同時用到apache的HttpClient
、Spring的RestTemplate
以及WebClient
等)。
這裡舉個Spring
中的例子。在Spring
的AOP
中,由於Advisor
需要的是MethodInterceptor
物件,所以每一個Advisor
中的Advice
都要適配成對應的MethodInterceptor
物件
public interface AdvisorAdapter {
boolean supportsAdvice(Advice advice);
MethodInterceptor getInterceptor(Advisor advisor);
}
class ThrowsAdviceAdapter implements AdvisorAdapter, Serializable{
@Override
public boolean supportsAdvice(Advice advice) {
return (advice instanceof ThrowsAdvice);
}
@Override
public MethodInterceptor getInterceptor(Advisor advisor) {
return new ThrowsAdviceInterceptor(advisor.getAdvice());
}
}
模式總結
介面卡模式將現有介面轉化為客戶類所期望的介面,實現了對現有類的複用,它是一種使用頻率非常高的設計模式,在軟體開發中得以廣泛應用。
主要優點
無論是物件介面卡模式還是類介面卡模式都具有如下優點:
(1) 將目標類和適配者類解耦,通過引入一個介面卡類來重用現有的適配者類,無須修改原有結構,提高了擴充套件性,符合“開閉原則”
(2) 增加了類的透明性和複用性,將具體的業務實現過程封裝在適配者類中,對於客戶端類而言是透明的,而且提高了適配者的複用性,同一個適配者類可以在多個不同的系統中複用。
具體來說,類介面卡
模式還有如下優點:
由於介面卡類是適配者類的子類,因此可以在介面卡類中置換一些適配者的方法,使得介面卡的靈活性更強。
物件介面卡
模式還有如下優點:
(1) 一個物件介面卡可以把多個不同的適配者適配到同一個目標;
(2) 可以適配一個適配者的子類,由於介面卡和適配者之間是關聯關係,根據“里氏代換原則”,適配者的子類也可通過該介面卡進行適配。
主要缺點
類介面卡
模式的缺點如下:
(1) 對於Java、C#等不支援多重類繼承的語言,一次最多隻能適配一個適配者類,不能同時適配多個適配者;
(2) 適配者類不能為最終類,如在Java中不能為final類,C#中不能為sealed類;
(3) 在Java、C#等語言中,類介面卡模式中的目標抽象類只能為介面,不能為類,其使用有一定的侷限性。
物件介面卡模式的缺點如下:
與類介面卡模式相比,要在介面卡中置換適配者類的某些方法比較麻煩(比如說適配者類中的某些方法是protected
,而我們做適配的時候剛好需要用到)。如果一定要置換掉適配者類的一個或多個方法,可以先做一個適配者類的子類,將適配者類的方法置換掉,然後再把適配者類的子類當做真正的適配者進行適配,實現過程較為複雜。
適用場景
在以下情況下可以考慮使用介面卡模式:
系統需要使用(複用)一些現有的類,而這些類的介面(如方法名)不符合系統的需要,甚至沒有這些類的原始碼等等,可使用介面卡模式協調諸多不相容結構的場景。