設計模式之適配器模式

語言: CN / TW / HK

本文通過老王使用紙質書籍閲讀小王使用電子書籍的故事,詳細説明設計模式中的結構型設計模式之適配器模式,分別對對象適配器和類適配器代碼實現,最後為了加深理解,會列舉適配器設計模式在JDK和Spring源碼中的應用。

讀者可以拉取完整代碼到本地進行學習,實現代碼均測試通過後上傳到 碼雲

一、引出問題

自從小王被老王趕出家門以後,老王過了幾天舒心的日子,在家裏的書架上買了許許多多的紙質書。

有一天,小王過夠了野人生活回來了,小王也是一個喜歡讀書的人,但是小王不喜歡紙質書,就要求老王將這些書換成電子版。

老王立馬就不開心了,這是我不知道花費多少個日夜才設計好的書架,給你換成電子版的不僅要花費我大量的精力改變原有書架的結構,再想找我想看的書得有多難,而且老李來了想看紙質版怎麼辦,我還要再換回去嗎?

小王隨即想到了一種解決思路:這些書現在符合你的風格,應該設計一種模式,讓這些書也能符合我的需求,讓我們倆可以在一起讀書,既不改變你的書架結構,又能擴展它的功能。

老王滿意的點了點頭,你説的不錯,這實際上就是結構型設計模式中的適配器模式。

二、概念與使用

引用Gof中對適配器設計模式的概念:將一個類的接口轉化成客户希望的另一個接口,由於接口不兼容而不能一起工作的類可以一起工作。

很顯然,在適配器設計模式中應該有三個角色。

目標類:Target,該角色把其他類轉換為我們期望的接口,可以是一個抽象類或接口,也可以是具體類。

被適配者類(源): Adaptee ,原有的接口,也是希望被適配的接口。

適配器: Adapter, 將被適配者和目標抽象類組合到一起的類。

在我們的實際案例中,老王的紙質書很明顯應該是屬於被適配者,小王的電子版就是目標類,適配器應該是能調用老王的紙質書,並使用一些相關的業務方法轉化成電子版,比如調用老王書之前買一個掃描儀,在老王書調出來以後掃描書籍。

既然適配器中要調用老王的紙質書,調用它的方法應該是有兩種實現方式。

一是直接繼承老王,那樣就可以直接調用老王的方法了。

二是在適配器中創建老王的對象,然後再調用老王的方法。

這其實對應了適配器的兩種方式,根據適配器類與適配者類的關係不同,適配器模式可分為對象適配器和類適配器兩種,在 對象適配器模式 中,適配器與適配者之間是 關聯 關係;在 類適配器模式 中,適配器與適配者之間是 繼承 (或實現)關係。

我們先看類適配器實現方式:

被適配者類:

/**
 * 源對象
 * @author tcy
 * @Date 04-08-2022
 */
public class AdapteePaperReading {

    public void readPaper(){
        System.out.println("這是老王讀的紙質書...(被適配者方法)");
    }
}

目標對象:

/**
 * 目標對象
 */
public interface TargetOnlineReading {
    public void ReadOnline();
}

適配器:

/**
 * @author tcy
 * @Date 04-08-2022
 */
public class Adapter extends AdapteePaperReading implements TargetOnlineReading{
    @Override
    public void ReadOnline() {

        System.out.println("買一個掃描儀...");
        readPaper();
        System.out.println("拿到紙質書掃描為電子書...");
    }

}

客户端:

/**
 * @author tcy
 * @Date 04-08-2022
 */
public class Client {

    public static void main(String[] args) {
        Adapter adapter=new Adapter();
        adapter.ReadOnline();

    }
}

以上就實現類適配器,如果我們要實現對象適配器也很簡單,目標對象和被適配者都不變,需要改變的是適配器代碼

/**
 * @author tcy
 * @Date 04-08-2022
 */
public class Adapter implements TargetOnlineReading {

    // 適配者是對象適配器的一個屬性
    private AdapteePaperReading adaptee = new AdapteePaperReading();

    @Override
    public void ReadOnline() {

        System.out.println("買一個掃描儀...");
        adaptee.readPaper();
        System.out.println("拿到紙質書掃描為電子書...");
    }
}

這樣老王和小王就能在一起讀書了。但這種方式只能作為系統的一種補救措施,而不是在系統設計之初就考慮這種方式,如果老王有十個八個兒子都要求按照他們的習慣來,那系統就會相當的複雜,無異於一場災難。而是應該考慮重做書架,將各種情況都考慮進去。

需要説明的是,類適配器之間的耦合度比後者高,且要求程序員瞭解現有組件庫中的相關組件的內部結構,所以應用相對較少些。

三、應用

案例有一些生硬,為了加深對適配器設計模式的把握,我們介紹該模式在Jdk源碼和Spring中的應用。

1、JDK應用

JDK使用適配器的典型例子是Java線程池FutureTask類。我們知道通過實現接口實現多線程一共有兩種方式,Runnable接口和Callable接口。

FutrueTask類中有兩個構造方法:

構造方法一:傳入參數為Callable接口

// 這是FutureTask的構造方法一
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;     
}

構造方法二:傳入的參數為Runnable接口

// 這是FutureTask的構造方法二
public FutureTask(Runnable runnable, V result) {
    // 調用Executors類中的callable方法進行轉化
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;   
}

在構造方法中實際上加傳入的Runnable任務在內部統一被轉換為Callable任務。

可以看到這裏採用的是適配器模式,調用 RunnableAdapter&lt;T&gt;(task, result) 方法來適配,實現如下:

static final class RunnableAdapter<T> implements Callable<T> {
    final Runnable task;
    final T result;
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        task.run();
        return result;
    }
}

這樣無論是傳入Runnalbe還是Callable都能適配任務,這個適配器很簡單,就是簡單的實現了Callable接口,在call()實現中調用Runnable.run()方法,然後把傳入的result作為任務的結果返回。

通過這麼一個簡單案例可以加深對適配器模式的理解。

2、SpringAOP應用

我們知道在Spring的Aop中,使用的 Advice(通知) 來增強被代理類的功能。

其中Advice的類型有:BeforeAdvice(在執行切點前的通知)、AfterReturningAdvice(在運行完切點完未返回之前)、ThrowsAdvice(在運行完切點時拋出異常進行的通知),AfterAdvice(執行完該切點後,進行的通知)、Around advice(包裹一個方法的執行)

在每個類型 Advice 都有對應的攔截器,MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor、 ThrowsAdviceInterceptor

Spring需要將每個 Advice 都封裝成對應的攔截器類型,返回給容器,這時候採用的就是適配器類型。

Advice 就相當於適配者,對應的攔截器類型就是目標類。

限於篇幅,有興趣的讀者可以到 Spring源碼中瞭解具體過程

3、SpringMVC應用

Spring MVC中的適配器模式主要用於執行目標 Controller 中的請求處理方法。

在Spring MVC中,DispatcherServlet 作為用户,HandlerAdapter 作為期望接口,具體的適配器實現類用於對目標類進行適配,Controller 作為需要適配的類。

當Spring容器啟動後,會將所有定義好的適配器對象存放在一個List集合中,當一個請求來臨時,DispatcherServlet 會通過 handler 的類型找到對應適配器,並將該適配器對象返回給用户,然後就可以統一通過適配器的 hanle() 方法來調用 Controller 中的用於處理請求的方法。

通過適配器模式我們將所有的 controller 統一交給 HandlerAdapter 處理,免去了寫大量的 if-else 語句對 Controller 進行判斷,也更利於擴展新的 Controller 類型。

單純的説蒼白無力,我們手寫實現SpringMVC的核心流程,完整代碼已經上傳到 碼雲

四、總結

既然適配器模式可以擴展原有類的功能,那它和代理模式在一定程度上不是重合了嗎?貌似擴展老王的書架使用代理模式同樣是可以實現。

其實我們看結構型設計模式的定義:結構型模式涉及到如何組合類和類以獲得更大的結構,結構型類模式採用 繼承機制來組** 合接口或實現**。

代理模式與適配器模式都分別有繼承、接口方式實現的子分類模式。基於接口實現的代理模式稱為靜態代理模式、JDK(動態)代理模式,基於繼承實現的代理模式稱為Cglib(動態)代理模式。

基於接口(同時含類繼承)實現的適配器模式稱為類適配器模式,(只)基於繼承(使用委託)實現的適配器模式稱為類適配器模式。

代理模式是為其他類提供一種代理以控制對這個類的訪問。我們不直接去接觸目標類,而是直接操作代理類,代理類再去操作目標類。因為不直接接觸目標類,因此我們可以在代理類的同名方法中添加或刪除功能模塊,而不用去修改目標類的原方法。

而適配器模式則主要是協調現實與需求的差異,減少對已有代碼的改動,適配不同的接口、類類型。

項目實施中可能會出現這樣的情況:當前已完成的項目的某一個包內的各個類實現了一些特定的接口,而客户提出了新的需求,要求實現他所指定的那些接口(拋棄原有的方法或接口),但其業務細節卻是相同、完全一樣的。此時,我們可能並不想複製粘貼原代碼到新的方法中去,這就需要將一個類的接口轉換成新需求的另一個接口。

實現方式有很多,沒有必要咬文嚼字糾結使用哪種設計模式,設計模式本身就是很相似,只要能簡潔開發流程,讓我們的代碼更好的工作就是完美的。具體使用哪一種就需要讀者熟練掌握各種設計模式了,並認真體會他們各自的優勢。

推薦讀者,參考 軟件設計七大原則 認真閲讀往期的文章,認真體會。

創建型設計模式:

一、設計模式之工廠方法和抽象工廠

二、設計模式之單例和原型

三、設計模式之建造者模式

結構型設計模式:

四、設計模式之代理模式