[01][01][12] 適配器模式詳解

[TOC]

1. 定義

適配器模式是指將一個類的接口轉換成客戶期望的另一個接口,使原本的接口不兼容的類可以一起工作

2. 適用場景

  • 已經(jīng)存在的類,它的方法和需求不匹配(方法結果相同或相似)的情況
  • 適配器模式不是軟件設計階段考慮的設計模式,是隨著軟件維護,由于不同產(chǎn)品,不同廠家造成功能類似而接口不相同情況下的解決方案

生活中也非常的應用場景,例如電源插轉換頭,手機充電轉換頭,顯示器轉接頭


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è)務場景,利用適配模式來解決實際問題.年紀稍微大一點的小伙伴一定經(jīng)歷過這樣一個過程.我們很早以前開發(fā)的老系統(tǒng)應該都有登錄接口,但是隨著業(yè)務的發(fā)展和社會的進步,單純地依賴用戶名密碼登錄顯然不能滿足用戶需求了.現(xiàn)在,我們大部分系統(tǒng)都已經(jī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、調(diào)用原來的登錄方法
        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ū)別不大?在這里我要強調(diào)一下,適配器模式主要解決的是功能兼容問題,單場景適配大家可能不會和策略模式有對比.但多場景適配大家產(chǎn)生聯(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 類,它也有多個子類,類圖如下


其適配調(diào)用的關鍵代碼還是在 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()方法中調(diào)用了 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)調(diào)用了 supports()方法判斷是否兼容,循環(huán)迭代集合中的 Adapter 又是在初始化時早已賦值.這里我們不再深入,后面的源碼專題中還會繼續(xù)講解

5. 優(yōu)缺點

5.1 優(yōu)點

  • 能提高類的透明性和復用,現(xiàn)有的類復用但不需要改變
  • 目標類和適配器類解耦,提高程序的擴展性
  • 在很多業(yè)務場景中符合開閉原則

5.2 缺點

  • 適配器編寫過程需要全面考慮,可能會增加系統(tǒng)的復雜性
  • 增加代碼閱讀難度,降低代碼可讀性,過多使用適配器會使系統(tǒng)代碼變得凌亂
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容