設計模式系列之適配器模式(Adapter Pattern)——不兼容結構的協調

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟體開發人員內功修鍊之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的部落格//blog.csdn.net/LoveLion/article/details/17517213

模式概述

模式定義

與電源適配器相似,在適配器模式中引入了一個被稱為適配器(Adapter)的包裝類,而它所包裝的對象稱為適配者(Adaptee),即被適配的類。適配器的實現就是把客戶類的請求轉化為對適配者的相應介面的調用。也就是說:當客戶類調用適配器的方法時,在適配器類的內部將調用適配者類的方法,而這個過程對客戶類是透明的,客戶類並不直接訪問適配者類。因此,適配器讓那些由於介面不兼容而不能交互的類可以一起工作。

適配器模式(Adapter Pattern): 將一個介面轉換成期望的另一個介面,使介面不兼容的那些類可以一起工作,其別名為包裝器(Wrapper)。適配器模式既可以作為類結構型模式,也可以作為對象結構型模式。

注意:在適配器模式定義中所提及的介面是指廣義的介面,它可以表示一個方法或者方法的集合。

模式結構圖

在適配器模式中,我們通過增加一個新的適配器類來解決介面不兼容的問題,使得原本沒有任何關係的類可以協同工作。根據適配器類與適配者類的關係不同,適配器模式可分為對象適配器類適配器兩種,在對象適配器模式中,適配器與適配者之間是關聯關係;在類適配器模式中,適配器與適配者之間是繼承關係。在實際開發中,對象適配器的使用頻率更高,對象適配器模式結構如圖所示

對象適配器結構圖

在對象適配器模式結構圖中包含如下幾個角色:

  • Target(目標抽象類):目標抽象類定義客戶所需介面,可以是一個抽象類或介面,也可以是具體類。
  • Adapter(適配器類):適配器可以調用另一個介面,作為一個轉換器,對AdapteeTarget進行適配,適配器類是適配器模式的核心,在對象適配器中,它通過繼承(或者實現)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();
	}
}

由於JavaC#等語言不支援多重類繼承,因此類適配器的使用受到很多限制,例如如果目標抽象類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類中也大量使用到了適配器模式。比如說StringReaderString適配到ReaderInputStreamReaderInputStream適配到Reader等等。

這裡用StringReader來說明。這裡的StringReader相當於上述的AdapterReader相當於上述的TargetString相當於上述的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的HttpClientOkHttpClient、Spring的RestTemplate以及WebClient等等)來實現,這種很自然的思想 個人覺得本質上也用到了適配器模式,相當於是把第三方的HttpClient適配成了自己的SpecialHttpClient

當轉換的源不是單一的時候,這種適配器思想就凸顯出來了(對應上面的例子就是說 項目中需要同時用到apache的HttpClient、Spring的RestTemplate以及WebClient等)。

這裡舉個Spring中的例子。在SpringAOP中,由於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,而我們做適配的時候剛好需要用到)。如果一定要置換掉適配者類的一個或多個方法,可以先做一個適配者類的子類,將適配者類的方法置換掉,然後再把適配者類的子類當做真正的適配者進行適配,實現過程較為複雜。

適用場景

在以下情況下可以考慮使用適配器模式:

系統需要使用(復用)一些現有的類,而這些類的介面(如方法名)不符合系統的需要,甚至沒有這些類的源程式碼等等,可使用適配器模式協調諸多不兼容結構的場景。