Java設(shè)計模式中的適配器模式詳解
一、概述
適配器模式(Adapter),是23種設(shè)計模式中的結(jié)構(gòu)型模式之一;它就像我們電腦上接口不夠時,需要用到的拓展塢,起到轉(zhuǎn)接的作用。它可以將新的功能和原先的功能連接起來,使由于需求變動導(dǎo)致不能用的功能,重新利用起來。
上圖的Mac上,只有兩個typec接口,當我們需要用到USB、網(wǎng)線、HDMI等接口時,這就不夠用了,所以我們需要一個拓展塢來增加電腦的接口
言歸正傳,下面來了解下適配器模式中的角色:請求者(client)、目標角色(Target)、源角色(Adaptee)、適配器角色(Adapter),這四個角色是保證這個設(shè)計模式運行的關(guān)鍵。
- client:需要使用適配器的對象,不需要關(guān)心適配器內(nèi)部的實現(xiàn),只對接目標角色。
- Target:目標角色,和client直接對接,定義了client需要用到的功能。
- Adaptee:需要被進行適配的對象。
- Adapter:適配器,負責將源對象轉(zhuǎn)化,給client做適配。
二、入門案例
適配器模式也分兩種:對象適配器、類適配器。其實兩種方式的區(qū)別在于,適配器類中的實現(xiàn),類適配器是通過繼承源對象的類,對象適配器是引用源對象的類。
當然兩種方式各有優(yōu)缺點,咱分別來說下;
類適配器:由于采用繼承模式,在適配器中可以重寫Adaptee原有的方法,使得適配器可以更加靈活;但是有局限性,Java是單繼承模式,所以適配器類只能繼承Adaptee,不能在額外繼承其他類,也導(dǎo)致Target類只能是接口。
對象適配器:這個模式規(guī)避了單繼承的劣勢,將Adaptee類用引用的方式傳遞給Adapter,這樣可以傳遞的是Adaptee對象本身及其子類對象,相比類適配器更加的開放;但是也正是因為這種開放性,導(dǎo)致需要自己重新定義Adaptee,增加額外的操作。
類適配器UML圖
對象適配器UML圖
下面,是結(jié)合上面電腦的場景,寫的一個入門案例,分別是四個類:Client
、Adaptee
、Adapter
、Target
,代表了適配器模式中的四種角色。
/** * @author * @version 1.0 * @date 2023/5/9 15:54 * @description:源角色 */ public class Adaptee { /** * 需要被適配的適配的功能 * 以Mac筆記本的typec接口舉例 */ public void typeC() { System.out.println("我只是一個typeC接口"); } }
/** * @author * @version 1.0 * @date 2023/5/9 15:57 * @description:目標接口 */ public interface Target { /** * 定義一個轉(zhuǎn)接功能的入口 */ void socket(); }
/** * @author * @version 1.0 * @date 2023/5/9 16:00 * @description:適配器 */ public class Adapter extends Adaptee implements Target { /** * 實現(xiàn)適配功能 * 以Mac的拓展塢為例,拓展更多的接口:usb、typc、網(wǎng)線插口... */ @Override public void socket() { typeC(); System.out.println("新增usb插口。。。"); System.out.println("新增網(wǎng)線插口。。。"); System.out.println("新增typec插口。。。"); } }
/** * @author * @version 1.0 * @date 2023/5/9 15:52 * @description:請求者 */ public class Client { public static void main(String[] args) { Target target = new Adapter(); target.socket(); } }
這個案例比較簡單,僅僅是一個入門的demo,也是類適配器模式的案例,采用繼承模式。在對象適配器模式中,區(qū)別就是Adapter
這個適配器類,采用的是組合模式,下面是對象適配器模式中Adapter
的代碼;
/** * @author * @version 1.0 * @date 2023/5/9 16:00 * @description:適配器 */ public class Adapter implements Target { private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee = adaptee; } /** * 實現(xiàn)適配功能 * 以Mac的拓展塢為例,拓展更多的接口:usb、typc、網(wǎng)線插口... */ @Override public void socket() { adaptee.typeC(); System.out.println("新增usb插口。。。"); System.out.println("新增網(wǎng)線插口。。。"); System.out.println("新增typec插口。。。"); } }
三、運用場景
其實適配器模式為何會存在,全靠“爛代碼”的襯托。在初期的設(shè)計上,一代目沒有考慮到后期的兼容性問題,只顧自己一時爽,那后期接手的人就會感覺到頭疼,就會有“還不如重寫這段代碼的想法”。但是這部分代碼往往都是經(jīng)過N代人的充分測試,穩(wěn)定性比較高,一時半會還不能對它下手。這時候我們的適配器模式就孕育而生,可以在不動用老代碼的前提下,實現(xiàn)新邏輯,并且能做二次封裝。這種場景,我在之前的系統(tǒng)重構(gòu)中深有體會,不說了,都是淚。
當然還存在一種情況,可以對不同的外部數(shù)據(jù)進行統(tǒng)一輸出。例如,寫一個獲取一些信息的接口,你對前端暴露的都是統(tǒng)一的返回字段,但是需要調(diào)用不同的外部api獲取不同的信息,不同的api返回給你的字段都是不同的,比如企業(yè)工商信息、用戶賬戶信息、用戶津貼信息等等。下面我對這種場景具體分析下;
首先,我定義一個接口,接收用戶id和數(shù)據(jù)類型兩個參數(shù),定義統(tǒng)一的輸出字段。
/** * @author * @version 1.0 * @date 2023/5/10 11:03 * @description */ @RestController @RequestMapping("/user") @RequiredArgsConstructor public class UserInfoController { private final UserInfoTargetService userInfoTargetService; @PostMapping("/info") public Result<DataInfoVo> queryInfo(@RequestParam Integer userId, @RequestParam String type) { return Result.success(userInfoTargetService.queryData(userId, type)); } }
定義統(tǒng)一的輸出的類DataInfoVo
,這里定義的字段需要暴露給前端,具體業(yè)務(wù)意義跟前端商定。
/** * @author * @version 1.0 * @date 2023/5/10 14:40 * @description */ @Data public class DataInfoVo { /** * 名稱 */ private String name; /** * 類型 */ private String type; /** * 預(yù)留字段:具體業(yè)務(wù)意義自行定義 */ private Object extInfo; }
然后,定義Target接口(篇幅原因,這里不做展示),Adapter適配器類,這里采用的是對象適配器,由于單繼承的限制,對象適配器也是最常用的適配器模式。
/** * @author * @version 1.0 * @date 2023/5/10 15:09 * @description */ @Service @RequiredArgsConstructor public class UserInfoAdapter implements UserInfoTargetService { /** * 源數(shù)據(jù)類管理器 */ private final AdapteeManager adapteeManager; @Override public DataInfoVo queryData(Integer userId, String type) { // 根據(jù)類型,得到唯一的源數(shù)據(jù)類 UserBaseAdaptee adaptee = adapteeManager.getAdaptee(type); if (Objects.nonNull(adaptee)) { Object data = adaptee.getData(userId, type); return adaptee.convert(data); } return null; } }
這里定義了一個AdapteeManager
類,表示管理Adaptee
類,內(nèi)部維護一個map,用于存儲真實Adaptee
類。
/** * @author 往事如風 * @version 1.0 * @date 2023/5/10 15:37 * @description */ public class AdapteeManager { private Map<String, UserBaseAdaptee> baseAdapteeMap; public void setBaseAdapteeMap(List<UserBaseAdaptee> adaptees) { baseAdapteeMap = adaptees.stream() .collect(Collectors.toMap(handler -> AnnotationUtils.findAnnotation(handler.getClass(), Adapter.class).type(), v -> v, (v1, v2) -> v1)); } public UserBaseAdaptee getAdaptee(String type) { return baseAdapteeMap.get(type); } }
最后,按照數(shù)據(jù)類型,定義了三個Adaptee類:AllowanceServiceAdaptee
(津貼)、BusinessServiceAdaptee
(企業(yè)工商)、UserAccountServiceAdaptee
(用戶賬戶)。
/** * @author * @version 1.0 * @date 2023/5/10 15:00 * @description */ @Adapter(type = "JT") public class AllowanceServiceAdaptee implements UserBaseAdaptee { @Override public Object getData(Integer userId, String type) { // 模擬調(diào)用外部api,查詢津貼信息 AllowanceVo allowanceVo = new AllowanceVo(); allowanceVo.setAllowanceType("管理津貼"); allowanceVo.setAllowanceAccount("xwqeretry2345676"); allowanceVo.setAmount(new BigDecimal(20000)); return allowanceVo; } @Override public DataInfoVo convert(Object data) { AllowanceVo preConvert = (AllowanceVo) data; DataInfoVo dataInfoVo = new DataInfoVo(); dataInfoVo.setName(preConvert.getAllowanceAccount()); dataInfoVo.setType(preConvert.getAllowanceType()); dataInfoVo.setExtInfo(preConvert.getAmount()); return dataInfoVo; } }
/** * @author * @version 1.0 * @date 2023/5/10 15:00 * @description */ @Adapter(type = "QY") public class BusinessServiceAdaptee implements UserBaseAdaptee { @Override public Object getData(Integer userId, String type) { // 模擬調(diào)用外部api,查詢企業(yè)工商信息 BusinessVo businessVo = new BusinessVo(); businessVo.setBusName("xxx科技有限公司"); businessVo.setBusCode("q24243Je54sdfd99"); businessVo.setBusType("中大型企業(yè)"); return businessVo; } @Override public DataInfoVo convert(Object data) { BusinessVo preConvert = (BusinessVo) data; DataInfoVo dataInfoVo = new DataInfoVo(); dataInfoVo.setName(preConvert.getBusName()); dataInfoVo.setType(preConvert.getBusType()); dataInfoVo.setExtInfo(preConvert.getBusCode()); return dataInfoVo; } }
/** * @author * @version 1.0 * @date 2023/5/10 15:00 * @description */ @Adapter(type = "YH") public class UserAccountServiceAdaptee implements UserBaseAdaptee { @Override public Object getData(Integer userId, String type) { // 模擬調(diào)用外部api,查詢企業(yè)工商信息 UserAccountVo userAccountVo = new UserAccountVo(); userAccountVo.setAccountNo("afsdfd1243567"); userAccountVo.setAccountType("銀行卡"); userAccountVo.setName("中國農(nóng)業(yè)銀行"); return userAccountVo; } @Override public DataInfoVo convert(Object data) { UserAccountVo preConvert = (UserAccountVo) data; DataInfoVo dataInfoVo = new DataInfoVo(); dataInfoVo.setName(preConvert.getName()); dataInfoVo.setType(preConvert.getAccountType()); dataInfoVo.setExtInfo(preConvert.getAccountNo()); return dataInfoVo; } }
這三個類都實現(xiàn)一個接口UserBaseAdaptee
,該接口定義了統(tǒng)一的規(guī)范
/** * @author * @version 1.0 * @date 2023/5/10 15:03 * @description */ public interface UserBaseAdaptee { /** * 獲取數(shù)據(jù) * @param userId * @param type * @return */ Object getData(Integer userId, String type); /** * 數(shù)據(jù)轉(zhuǎn)化為統(tǒng)一的實體 * @param data * @return */ DataInfoVo convert(Object data); }
這些類中,其實重點看下UserInfoAdapter
適配器類,這里做的操作是通過源數(shù)據(jù)類,拿到外部返回的數(shù)據(jù),最后將不同的數(shù)據(jù)轉(zhuǎn)化為統(tǒng)一的字段,返回出去。
這里我沒有按照固定的模式,稍加了改變。將適配器類中引用源數(shù)據(jù)類的方式,改成將源數(shù)據(jù)類加入map中暫存,最后通過前端傳輸?shù)膖ype字段來獲取源數(shù)據(jù)類,這也是對象適配器比較靈活的一種體現(xiàn)。
四、源碼中的運用
在JDK的源碼中,JUC下有個類FutureTask
,其中它的一段構(gòu)造方法如下:
public class FutureTask<V> implements RunnableFuture<V> { public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; // ensure visibility of callable } public FutureTask(Runnable runnable, V result) { this.callable = Executors.callable(runnable, result); this.state = NEW; // ensure visibility of callable } }
其中一個構(gòu)造函數(shù)中,callable是通過Executors類的方法進行適配的,通過一個RunnableAdapter的適配器類,進行包裝并返回
public static <T> Callable<T> callable(Runnable task, T result) { if (task == null) throw new NullPointerException(); return new 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; } }
這樣的話,無論傳入Runnable還是Callable都可以適配任務(wù),雖然看著是調(diào)用了Callable的call方法,實際內(nèi)部是調(diào)用了Runnable的run方法,并且將傳入的返回數(shù)據(jù)返回給外部使用。
五、總結(jié)
適配器模式其實是一個比較好理解的設(shè)計模式,但是對于大多數(shù)初學者而言,就會很容易看一遍之后立馬忘,這是缺少實際運用造成的。其實編程主要考察的還是我們的一種思維模式,就像這個適配器模式,理解它的運用場景最重要。如果給你一個業(yè)務(wù)場景,你能在腦海中有大致的設(shè)計思路或者解決方案,那你就已經(jīng)掌握精髓了。至于具體的落地,有些細節(jié)忘記也是在所難免,翻翻資料就會立馬回到腦海中。
最后,每次遇到問題,用心總結(jié),你會離成功更近一步。
六、參考源碼
編程文檔: https://gitee.com/cicadasmile/butte-java-note 應(yīng)用倉庫: https://gitee.com/cicadasmile/butte-flyer-parent
以上就是Java設(shè)計模式中的適配器模式詳解的詳細內(nèi)容,更多關(guān)于Java 適配器模式的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring中@Controller和@RestController的區(qū)別詳解
這篇文章主要介紹了Spring中@Controller和@RestController的區(qū)別詳解,@RestController?是?@Controller?和?@ResponseBody?的結(jié)合體,單獨使用?@RestController?的效果與?@Controller?和?@ResponseBody?二者同時使用的效果相同,需要的朋友可以參考下2023-10-10基于Java解決華為機試實現(xiàn)整數(shù)與IP地址間的轉(zhuǎn)換?
這篇文章主要介紹了基于Java解決華為機試實現(xiàn)整數(shù)與IP地址間的轉(zhuǎn)換,文章舉例說明圍繞文章主題展開相關(guān)內(nèi)容,具有一定的參考價值,需要的小伙伴可以參考一下2022-02-02Spring線程池ThreadPoolTaskExecutor的用法及說明
這篇文章主要介紹了Spring線程池ThreadPoolTaskExecutor的用法及說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07springboot2.x解決運行順序及Bean對象注入順序的問題
這篇文章主要介紹了springboot2.x解決運行順序及Bean對象注入順序的問題,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01Java面向?qū)ο蟪绦蛟O(shè)計多態(tài)性示例
這篇文章主要介紹了Java面向?qū)ο蟪绦蛟O(shè)計多態(tài)性,結(jié)合實例形式分析了java多態(tài)性的概念、原理、定義與使用方法及相關(guān)注意事項,需要的朋友可以參考下2018-03-03