[TOC]
1. 定義
適配器模式是指將一個類的接口轉換成客戶期望的另一個接口,使原本的接口不兼容的類可以一起工作
2. 適用場景
- 已經存在的類,它的方法和需求不匹配(方法結果相同或相似)的情況
- 適配器模式不是軟件設計階段考慮的設計模式,是隨著軟件維護,由于不同產品,不同廠家造成功能類似而接口不相同情況下的解決方案
生活中也非常的應用場景,例如電源插轉換頭,手機充電轉換頭,顯示器轉接頭
3. 代碼實現(xiàn)
在中國民用電都是 220V 交流電,但我們手機使用的鋰電池使用的 5V 直流電.因此,我們給手機充電時就需要使用電源適配器來進行轉換.下面我們有代碼來還原這個生活場景
- 創(chuàng)建 AC220 類,表示 220V 交流電
public class AC220 {
public int outputAC220() {
int output = 220;
System.out.println("輸出電壓" + output + "V");
return output;
}
}
- 創(chuàng)建 DC5 接口,表示 5V 直流電的標準
public interface DC5 {
/**
* 輸出 5V 電壓
* @return
*/
int outputDC5();
}
- 創(chuàng)建電源適配器 PowerAdapter 類
public class PowerAdapter implements DC5 {
private AC220 ac220;
public PowerAdapter(AC220 ac220) {
this.ac220 = ac220;
}
/**
* 輸出 5V 電壓
*
* @return
*/
@Override
public int outputDC5() {
int adapterInput = ac220.outputAC220();
int adapterOutput = adapterInput / 44;
System.out.println("使用 PowerAdapter 將輸入 AC: " + adapterInput + "V, 輸出 DC: " + adapterOutput + "V");
return adapterOutput;
}
}
- 測試代碼
public class PowerAdapterTest {
public static void main(String[] args) {
DC5 dc5 = new PowerAdapter(new AC220());
dc5.outputDC5();
}
}
運行結果
輸出電壓 220V
使用 PowerAdapter 將輸入 AC: 220V, 輸出 DC: 5V
上面的案例中,通過增加 PowerAdapter 電源適配器,實現(xiàn)了二者的兼容
4. 重構第三登錄自由適配的業(yè)務場景
下面我們來一個實際的業(yè)務場景,利用適配模式來解決實際問題.年紀稍微大一點的小伙伴一定經歷過這樣一個過程.我們很早以前開發(fā)的老系統(tǒng)應該都有登錄接口,但是隨著業(yè)務的發(fā)展和社會的進步,單純地依賴用戶名密碼登錄顯然不能滿足用戶需求了.現(xiàn)在,我們大部分系統(tǒng)都已經支持多種登錄方式,如 QQ 登錄,微信登錄,手機登錄,微博登錄等等,同時保留用戶名密碼的登錄方式.雖然登錄形式豐富了,但是登錄后的處理邏輯可以不必改,同樣是將登錄狀態(tài)保存到 session,遵循開閉原則
- 創(chuàng)建統(tǒng)一的返回結果 ResultMsg 類
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultMsg {
private int code;
private String msg;
private Object data;
}
- 假設老系統(tǒng)的登錄邏輯 SignService
public class SignService {
/**
* 注冊方法
*/
public ResultMsg register(String username, String password) {
return new ResultMsg(200, "注冊成功", new Member());
}
/**
* 登錄的方法
*/
public ResultMsg login(String username, String password) {
return null;
}
}
為了遵循開閉原則,老系統(tǒng)的代碼我們不會去修改.那么下面開啟代碼重構之路
- 創(chuàng)建 Member 類
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
private String username;
private String password;
private String mid;
private String info;
}
- 創(chuàng)建一個新的類繼承原來的邏輯,運行非常穩(wěn)定的代碼我們不去改動
public class SignInForThirdService extends SignService {
/**
* QQ 登錄
*/
public ResultMsg loginForQQ(String openId) {
// 1、openId 是全局唯一铁坎,我們可以把它當做是一個用戶名(加長)
// 2、密碼默認為 QQ_EMPTY
// 3、注冊(在原有系統(tǒng)里面創(chuàng)建一個用戶)
// 4句携、調用原來的登錄方法
return loginForRegister(openId, null);
}
/**
* WetChat 登錄
*/
public ResultMsg loginForWeChat(String openId) {
return null;
}
/**
* Token 登錄
*/
public ResultMsg loginForToken(String token) {
// 通過 token 拿到用戶信息疗锐,然后再重新登陸了一次
return null;
}
/**
* 手機號碼登錄
*/
public ResultMsg loginForTelephone(String telephone, String code) {
return null;
}
public ResultMsg loginForRegister(String username, String password) {
super.register(username, password);
return super.login(username, password);
}
}
- 測試代碼
public class SigninForThirdServiceTest {
public static void main(String[] args) {
SignInForThirdService service = new SignInForThirdService();
// 不改變原來的代碼衡载,也要能夠兼容新的需求
// 還可以再加一層策略模式
service.loginForQQ("sdfgdgfwresdf9123sdf");
}
}
通過這么一個簡單的適配,完成了代碼兼容.當然,我們代碼還可以更加優(yōu)雅,根據(jù)不同的登錄方式,創(chuàng)建不同的 Adapter
- 創(chuàng)建 LoginAdapter 接口
public interface LoginAdapter {
boolean support(Object adapter);
ResultMsg login(String id, Object adapter);
}
- 分別實現(xiàn)不同的登錄適配,QQ 登錄 LoginForQQAdapter
public class LoginForQQAdapter implements LoginAdapter {
public boolean support(Object adapter) {
return adapter instanceof LoginForQQAdapter;
}
public ResultMsg login(String id, Object adapter) {
return null;
}
}
- 新浪微博登錄 LoginForSinaAdapter
public class LoginForSinaAdapter implements LoginAdapter {
public boolean support(Object adapter) {
return adapter instanceof LoginForSinaAdapter;
}
public ResultMsg login(String id, Object adapter) {
return null;
}
}
- 手機號登錄 LoginForTelAdapter
public class LoginForTelAdapter implements LoginAdapter {
public boolean support(Object adapter) {
return adapter instanceof LoginForTelAdapter;
}
public ResultMsg login(String id, Object adapter) {
return null;
}
}
- Token 自動登錄 LoginForTokenAdapter
public class LoginForTokenAdapter implements LoginAdapter {
public boolean support(Object adapter) {
return adapter instanceof LoginForTokenAdapter;
}
public ResultMsg login(String id, Object adapter) {
return null;
}
}
- 微信登錄 LoginForWeChatAdapter
public class LoginForWeChatAdapter implements LoginAdapter {
public boolean support(Object adapter) {
return adapter instanceof LoginForWeChatAdapter;
}
public ResultMsg login(String id, Object adapter) {
return null;
}
}
- 創(chuàng)建第三方登錄兼容接口 IPassportForThird
public interface IPassportForThird {
/**
* QQ 登錄
*/
ResultMsg loginForQQ(String id);
/**
* 微信登錄
*/
ResultMsg loginForWeChat(String id);
/**
* 記住登錄狀態(tài)后自動登錄
*/
ResultMsg loginForToken(String token);
/**
* 手機號登錄
*/
ResultMsg loginForTelephone(String telephone, String code);
/**
* 注冊后自動登錄
*/
ResultMsg loginForRegister(String username, String passport);
}
- 實現(xiàn)兼容 PassportForThirdAdapter
public class PassportForThirdAdapter extends SignService implements IPassportForThird {
public ResultMsg loginForQQ(String id) {
return processLogin(id, LoginForQQAdapter.class);
}
public ResultMsg loginForWeChat(String id) {
return processLogin(id, LoginForWeChatAdapter.class);
}
public ResultMsg loginForToken(String token) {
return processLogin(token, LoginForTokenAdapter.class);
}
public ResultMsg loginForTelephone(String telephone, String code) {
return processLogin(telephone, LoginForTelAdapter.class);
}
public ResultMsg loginForRegister(String username, String password) {
super.register(username, password);
return super.login(username, password);
}
//這里用到了簡單工廠模式及策略模式
private ResultMsg processLogin(String key, Class<? extends LoginAdapter> clazz) {
try {
LoginAdapter adapter = clazz.newInstance();
if (adapter.support(adapter)) {
return adapter.login(key, adapter);
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
- 測試代碼
public class PassportTest {
public static void main(String[] args) {
IPassportForThird passportForThird = new PassportForThirdAdapter();
passportForThird.loginForQQ("");
}
}
至此,我們在遵循開閉原則的前提下,完整地實現(xiàn)了一個兼容多平臺登錄的業(yè)務場景.當然,我目前的這個設計也并不完美,僅供參考,感興趣的小伙伴可以繼續(xù)完善這段代碼.例如適配器中的參數(shù)目前是寫死為 String,改為 Object[]應該更合理.學習到這里,相信小伙伴會有一個疑問了:適配器模式跟策略模式好像區(qū)別不大?在這里我要強調一下,適配器模式主要解決的是功能兼容問題,單場景適配大家可能不會和策略模式有對比.但多場景適配大家產生聯(lián)想和混淆了.其實,大家有沒有發(fā)現(xiàn)一個細節(jié),我給每個適配器都加上了一個 support()方法,用來判斷是否兼容,support()方法的參數(shù)也是 Object 的,而 supoort()來自于接口.適配器的實現(xiàn)邏輯并不依賴于接口,我們完全可以將 LoginAdapter 接口去掉.而加上接口,只是為了代碼規(guī)范.上面的代碼可以說是策略模式,簡單工廠模式和適配器模式的綜合運用
4. 源碼分析
4.1 Spring 的 HandlerAdapter
Spring 中適配器模式也應用得非常廣泛,例如:SpringAOP 中的 AdvisorAdapter 類,它有三個實現(xiàn)類 MethodBeforeAdviceAdapter,AfterReturningAdviceAdapter 和 ThrowsAdviceAdapter
- 先來看頂層接口 AdvisorAdapter 的源代碼
public interface AdvisorAdapter {
boolean supportsAdvice(Advice var1);
MethodInterceptor getInterceptor(Advisor var1);
}
- 再看 MethodBeforeAdviceAdapter 類
class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {
MethodBeforeAdviceAdapter() {
}
public boolean supportsAdvice(Advice advice) {
return advice instanceof MethodBeforeAdvice;
}
public MethodInterceptor getInterceptor(Advisor advisor) {
MethodBeforeAdvice advice = (MethodBeforeAdvice)advisor.getAdvice();
return new MethodBeforeAdviceInterceptor(advice);
}
}
其它兩個類我這里就不把代碼貼出來了.Spring 會根據(jù)不同的 AOP 配置來確定使用對應的 Advice,跟策略模式不同的一個方法可以同時擁有多個 Advice
下面再來看一個 SpringMVC 中的 HandlerAdapter 類,它也有多個子類,類圖如下
其適配調用的關鍵代碼還是在 DispatcherServlet 的 doDispatch()方法中,下面我們還是來看源碼
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (this.logger.isDebugEnabled()) {
this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) &&
return;
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception) dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new
NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
- 在 doDispatch()方法中調用了 getHandlerAdapter()方法,來看代碼
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if(this.handlerAdapters != null) {
Iterator var2 = this.handlerAdapters.iterator();
while(var2.hasNext()) {
HandlerAdapter ha = (HandlerAdapter)var2.next();
if(this.logger.isTraceEnabled()) {
this.logger.trace("Testing handler adapter [" + ha + "]");
}
if(ha.supports(handler)) {
return ha;
}
}
}
throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
在 getHandlerAdapter()方法中循環(huán)調用了 supports()方法判斷是否兼容,循環(huán)迭代集合中的 Adapter 又是在初始化時早已賦值.這里我們不再深入,后面的源碼專題中還會繼續(xù)講解
5. 優(yōu)缺點
5.1 優(yōu)點
- 能提高類的透明性和復用,現(xiàn)有的類復用但不需要改變
- 目標類和適配器類解耦,提高程序的擴展性
- 在很多業(yè)務場景中符合開閉原則
5.2 缺點
- 適配器編寫過程需要全面考慮,可能會增加系統(tǒng)的復雜性
- 增加代碼閱讀難度,降低代碼可讀性,過多使用適配器會使系統(tǒng)代碼變得凌亂