設計模式之適配器模式
本文通過老王使用紙質書籍閲讀小王使用電子書籍的故事,詳細説明設計模式中的結構型設計模式之適配器模式,分別對對象適配器和類適配器代碼實現,最後為了加深理解,會列舉適配器設計模式在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<T>(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(動態)代理模式。
基於接口(同時含類繼承)實現的適配器模式稱為類適配器模式,(只)基於繼承(使用委託)實現的適配器模式稱為類適配器模式。
代理模式是為其他類提供一種代理以控制對這個類的訪問。我們不直接去接觸目標類,而是直接操作代理類,代理類再去操作目標類。因為不直接接觸目標類,因此我們可以在代理類的同名方法中添加或刪除功能模塊,而不用去修改目標類的原方法。
而適配器模式則主要是協調現實與需求的差異,減少對已有代碼的改動,適配不同的接口、類類型。
項目實施中可能會出現這樣的情況:當前已完成的項目的某一個包內的各個類實現了一些特定的接口,而客户提出了新的需求,要求實現他所指定的那些接口(拋棄原有的方法或接口),但其業務細節卻是相同、完全一樣的。此時,我們可能並不想複製粘貼原代碼到新的方法中去,這就需要將一個類的接口轉換成新需求的另一個接口。
實現方式有很多,沒有必要咬文嚼字糾結使用哪種設計模式,設計模式本身就是很相似,只要能簡潔開發流程,讓我們的代碼更好的工作就是完美的。具體使用哪一種就需要讀者熟練掌握各種設計模式了,並認真體會他們各自的優勢。
推薦讀者,參考 軟件設計七大原則 認真閲讀往期的文章,認真體會。
創建型設計模式:
結構型設計模式:
- 設計模式之狀態模式
- 如何實現數據庫讀一致性
- 我是怎麼入行做風控的
- C 11精要:部分語言特性
- 吳恩達來信:人工智能領域的求職小 tips
- EasyCV帶你復現更好更快的自監督算法-FastConvMAE
- 某車聯網App 通訊協議加密分析(四) Trace Code
- 帶你瞭解CANN的目標檢測與識別一站式方案
- EasyNLP玩轉文本摘要(新聞標題)生成
- PostgreSQL邏輯複製解密
- 基於 CoreDNS 和 K8s 構建雲原生場景下的企業級 DNS
- 循環神經網絡(RNN)可是在語音識別、自然語言處理等其他領域中引起了變革!
- 技術分享| 分佈式系統中服務註冊發現組件的原理及比較
- 利用谷歌地圖採集外貿客户的電話和手機號碼
- 跟我學Python圖像處理丨關於圖像金字塔的圖像向下取樣和向上取樣
- 帶你掌握如何使用CANN 算子ST測試工具msopst
- 一招教你如何高效批量導入與更新數據
- 一步步搞懂MySQL元數據鎖(MDL)
- 你知道如何用 PHP 實現多進程嗎?
- KubeSphere 網關的設計與實現(解讀)