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

[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)代碼變得凌亂
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末搔耕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子痰娱,更是在濱河造成了極大的恐慌弃榨,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梨睁,死亡現(xiàn)場離奇詭異鲸睛,居然都是意外死亡,警方通過查閱死者的電腦和手機坡贺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進店門官辈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來划咐,“玉大人,你說我怎么就攤上這事钧萍。” “怎么了政鼠?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵风瘦,是天一觀的道長。 經常有香客問我公般,道長万搔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任官帘,我火速辦了婚禮瞬雹,結果婚禮上,老公的妹妹穿的比我還像新娘刽虹。我一直安慰自己酗捌,他們只是感情好,可當我...
    茶點故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布涌哲。 她就那樣靜靜地躺著胖缤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪阀圾。 梳的紋絲不亂的頭發(fā)上哪廓,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天,我揣著相機與錄音初烘,去河邊找鬼涡真。 笑死,一個胖子當著我的面吹牛肾筐,可吹牛的內容都是我干的哆料。 我是一名探鬼主播,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼吗铐,長吁一口氣:“原來是場噩夢啊……” “哼剧劝!你這毒婦竟也來了?” 一聲冷哼從身側響起抓歼,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤讥此,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谣妻,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萄喳,經...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年蹋半,在試婚紗的時候發(fā)現(xiàn)自己被綠了他巨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖染突,靈堂內的尸體忽然破棺而出捻爷,到底是詐尸還是另有隱情,我是刑警寧澤份企,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布也榄,位于F島的核電站,受9級特大地震影響司志,放射性物質發(fā)生泄漏甜紫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一骂远、第九天 我趴在偏房一處隱蔽的房頂上張望囚霸。 院中可真熱鬧,春花似錦激才、人聲如沸拓型。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吨述。三九已至,卻和暖如春钞脂,著一層夾襖步出監(jiān)牢的瞬間揣云,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工冰啃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留邓夕,地道東北人。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓阎毅,卻偏偏與公主長得像焚刚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子扇调,可洞房花燭夜當晚...
    茶點故事閱讀 45,446評論 2 359