适配器模式(Adapter Pattern)是一種結構型模式,基于該模式設計的類能夠在兩個或者多個不兼容的類之間起到溝通橋梁的作用。
轉換插頭就是一個适配器的典型例子。不同的轉換插頭能都夠适配不同國家的插座标準,從而使得一個電器能在各個國家使用。
适配器的思想在程序設計中非常常見,下面代碼就體現了這種思想:
//方法一
public<K,V>Map<K,V>selectMap(Stringstatement,StringmapKey){
returnthis.selectMap(statement,null,mapKey,RowBounds.DEFAULT);
}
//方法二
public<K,V>Map<K,V>selectMap(Stringstatement,Objectparameter,StringmapKey){
returnthis.selectMap(statement,parameter,mapKey,RowBounds.DEFAULT);
}
//方法三
public<K,V>Map<K,V>selectMap(Stringstatement,Objectparameter,StringmapKey,RowBoundsrowBounds){
finalList<?extendsV>list=selectList(statement,parameter,rowBounds);
finalDefaultMapResultHandler<K,V>mapResultHandler=newDefaultMapResultHandler<>(mapKey,
configuration.getObjectFactory(),configuration.getObjectWrapperFactory(),configuration.getReflectorFactory());
finalDefaultResultContext<V>context=newDefaultResultContext<>();
for(Vo:list){
context.nextResultObject(o);
mapResultHandler.handleResult(context);
}
returnmapResultHandler.getMappedResults();
}
上述代碼中,方法三是核心方法,它需要四個輸入參數。而有些場景下,調用方隻能提供三個參數或者兩個參數。為了使得隻有三個參數或者兩個參數的調用方能夠正常地調用核心方法,方法一和方法二充當了方法适配器的作用。這兩個适配器通過為未知參數設置默認值的方式,搭建起了調用方和核心方法之間的橋梁。
不過,通常我們說起适配器模式是指類适配器或者對象适配器。(均參考自書籍《通用源碼閱讀指導書——MyBatis源碼詳解》)
2. 類适配器下圖給出了類适配器的類圖。
上圖中,Target接口是Client想調用的标準接口,而Adaptee是提供服務但不符合标準接口的目标類。Adapter便是為了Client能順利調用Adaptee而創建的适配器類。如下面代碼所示,Adapter即實現了Target接口又繼承了Adaptee類,從而使得Client能夠與Adaptee适配。
publicclassAdapterextendsAdapteeimplementsTarget{
@Override
publicvoidsayHi(){
super.sayHello();
}
}
對象适配器Adaptee不再繼承目标類,而是直接持有一個目标類的對象。下圖給出了對象适配器的類圖。
下面便給出了使用對象适配器的示例。
publicclassAdapterimplementsTarget{
//目标類的對象
privateAdapteeadaptee;
//初始化适配器時可以指定目标類對象
publicAdapter(Adapteeadaptee){
this.adaptee=adaptee;
}
@Override
publicvoidsayHi(){
adaptee.sayHello();
}
}
這樣,Adapter可以直接将Client要求的操作委托給目标類對象處理,也實現了Client和Adaptee之間的适配。而且這種适配器更為靈活一些,因為要适配的目标對象是作為初始化參數傳給Adapter的,更為靈活一些。
适配器模式能夠使得原本不兼容的類可以一起工作。通常情況下,如果目标類是可以修改的,則不需要使用适配器模式,直接修改目标類即可。但如果目标類是不可以修改的(例如目标類由外部提供,或者目标類被衆多其他類依賴必須保持不變),那麼适配器模式則會非常有用。
4. MyBatis中适配器模式的使用MyBatis在打印運行日志時大量使用了适配器模式。這是因為日志框架都是外部提供的,例如log4j、Logging、commons-logging、slf4j、logback等等,MyBatis要做的是對接這些日志框架,并通過它們将日志打印出來。在這個過程中,适配器必不可少。
因此,除了少量的裝飾器外,MyBatis的Log接口的實現類都是對象适配器,最終的實際工作要委托給被适配的目标對象來完成。因此是否存在一個可用的目标對象成了适配器能否正常工作的關鍵所在。于是LogFactory的主要工作就是嘗試生成各個目标對象。如果一個目标對象能夠被生成出來,那該目标對象對應的适配器就是可用的。
LogFactory生成目标對象的工作在靜态代碼塊中被觸發。下面代碼展示了LogFactory的靜态代碼塊。
static{
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
我們首先查看下代碼中的tryImplementation方法:
/**
*嘗試實現一個日志實例
*@paramrunnable用來嘗試實現日志實例的操作
*/
privatestaticvoidtryImplementation(Runnablerunnable){
if(logConstructor==null){
try{
runnable.run();
}catch(Throwablet){
//ignore
}
}
}
tryImplementation方法會在logConstructor為null的情況下調用Runnable對象的run方法。要注意一點,直接調用Runnable的run方法并不會觸發多線程,因此LogFactory中的多個tryImplementation方法是依次執行的。這也意味着useNoLogging方法中引用的NoLoggingImpl實現是最後的保底實現,而且NoLoggingImpl不需要被适配對象的支持,一定能夠成功。因此,最終的保底日志方案就就是不輸出日志。
我們以“tryImplementation(LogFactory::useCommonsLogging)”為例繼續追蹤源碼,該方法通過useCommonsLogging方法調用到了setImplementation方法。下面是setImplementation方法的帶注釋源碼。
/**
*設置日志實現
*@paramimplClass日志實現類
*/
privatestaticvoidsetImplementation(Class<?extendsLog>implClass){
try{
//當前日志實現類的構造方法
Constructor<?extendsLog>candidate=implClass.getConstructor(String.class);
//嘗試生成日志實現類的實例
Loglog=candidate.newInstance(LogFactory.class.getName());
if(log.isDebugEnabled()){
log.debug("Logginginitializedusing'" implClass "'adapter.");
}
//如果運行到這裡,說明沒有異常發生。則實例化日志實現類成功。
logConstructor=candidate;
}catch(Throwablet){
thrownewLogException("ErrorsettingLogimplementation.Cause:" t,t);
}
}
上述代碼顯示setImplementation方法會嘗試獲取參數中類的構造函數,并用這個構造函數創建一個日志記錄器。如果這次創建是成功的,則意味着以後的創建也是成功的,即當前參數中的類是可用的。因此把參數中類的構造方法賦給了logConstructor屬性。這樣,當外部調用getLog方法時,便可以由logConstructor創建出一個Log類的實例。
在靜态代碼塊中,我們發現StdOutImpl類并沒有參與設置logConstructor屬性的過程,這是因為它不在默認日志輸出方式的備選列表中。不過這并不代表着它毫無用處,因為MyBatis允許我們自行指定日志實現類。例如,我們在配置文件的settings節點下配置如下信息,則可以自定義StdOutImpl類作為日志輸出方式,使得MyBatis的日志輸出到控制台上。(均參考自書籍《通用源碼閱讀指導書——MyBatis源碼詳解》)
<settingname="logImpl"value="STDOUT_LOGGING"/>
自行指定日志實現類是在XML解析階段通過調用LogFactory中的useCustomLogging方法實現的。它相比于靜态代碼塊中的方法執行的更晚,會覆蓋前面的操作,因此具有更高的優先級。
通過MyBatis的源碼,我們了解了适配器模式的使用以及他們如何發揮作用。并且,還了解了MyBatis中日志實現的配置方法及其生效原理。可見,閱讀源碼對于深入理解設計模式的實現大有裨益。這裡,推薦大家閱讀《通用源碼閱讀指導書——MyBatis源碼詳解》。
《通用源碼閱讀指導書——MyBatis源碼詳解》是一本以MyBatis的源碼為實例講述源碼閱讀方法的書籍,并且附帶有示例項目源碼,MyBatis的全中文注解。書籍還總結了大量的編程知識和架構經驗,對提升編程和架構能力十分有用,非常推薦。
最後,我是高級架構師「易哥」,這裡是「架構研究所」。真心希望本文能讓大家有所收獲。
歡迎「關注」我們,我會偶爾出沒分享「軟件架構」和「編程」相關的幹貨知識。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!