設計模式 | 介面卡模式及典型應用

小旋鋒發表於2018-09-19

前言

本文介紹介面卡模式,原始碼分析spring aop, jpa, mvc中的介面卡模式

推薦閱讀

設計模式 | 簡單工廠模式及典型應用
設計模式 | 工廠方法模式及典型應用
設計模式 | 抽象工廠模式及典型應用
設計模式 | 建造者模式及典型應用
設計模式 | 原型模式及典型應用
設計模式 | 外觀模式及典型應用
設計模式 | 裝飾者模式及典型應用

更多內容可訪問我的個人部落格:laijianfeng.org

關注【小旋鋒】微信公眾號,及時接收博文推送

關注【小旋鋒】微信公眾號

介面卡模式

介面卡模式(Adapter Pattern):將一個介面轉換成客戶希望的另一個介面,使介面不相容的那些類可以一起工作,其別名為包裝器(Wrapper)。介面卡模式既可以作為類結構型模式,也可以作為物件結構型模式。

在介面卡模式中,我們通過增加一個新的介面卡類來解決介面不相容的問題,使得原本沒有任何關係的類可以協同工作。

根據介面卡類與適配者類的關係不同,介面卡模式可分為物件介面卡和類介面卡兩種,在物件介面卡模式中,介面卡與適配者之間是關聯關係;在類介面卡模式中,介面卡與適配者之間是繼承(或實現)關係。

角色

Target(目標抽象類):目標抽象類定義客戶所需介面,可以是一個抽象類或介面,也可以是具體類。

Adapter(介面卡類):介面卡可以呼叫另一個介面,作為一個轉換器,對Adaptee和Target進行適配,介面卡類是介面卡模式的核心,在物件介面卡中,它通過繼承Target並關聯一個Adaptee物件使二者產生聯絡。

Adaptee(適配者類):適配者即被適配的角色,它定義了一個已經存在的介面,這個介面需要適配,適配者類一般是一個具體類,包含了客戶希望使用的業務方法,在某些情況下可能沒有適配者類的原始碼。

預設介面卡模式(Default Adapter Pattern):當不需要實現一個介面所提供的所有方法時,可先設計一個抽象類實現該介面,併為介面中每個方法提供一個預設實現(空方法),那麼該抽象類的子類可以選擇性地覆蓋父類的某些方法來實現需求,它適用於不想使用一個介面中的所有方法的情況,又稱為單介面介面卡模式。預設介面卡模式是介面卡模式的一種變體,其應用也較為廣泛。在JDK類庫的事件處理包java.awt.event中廣泛使用了預設介面卡模式,如WindowAdapter、KeyAdapter、MouseAdapter等。

示例

類介面卡

首先有一個已存在的將被適配的類

public class Adaptee {
    public void adapteeRequest() {
        System.out.println("被適配者的方法");
    }
}
複製程式碼

定義一個目標介面

public interface Target {
    void request();
}
複製程式碼

怎麼才可以在目標介面中的 request() 呼叫 AdapteeadapteeRequest() 方法呢?

如果直接實現 Target 是不行的

public class ConcreteTarget implements Target {
    @Override
    public void request() {
        System.out.println("concreteTarget目標方法");
    }
}
複製程式碼

如果通過一個介面卡類,實現 Target 介面,同時繼承了 Adaptee 類,然後在實現的 request() 方法中呼叫父類的 adapteeRequest() 即可實現

public class Adapter extends Adaptee implements Target{
    @Override
    public void request() {
        //...一些操作...
        super.adapteeRequest();
        //...一些操作...
    }
}
複製程式碼

我們來測試一下

public class Test {
    public static void main(String[] args) {
        Target target = new ConcreteTarget();
        target.request();

        Target adapterTarget = new Adapter();
        adapterTarget.request();
    }
}
複製程式碼

輸出

concreteTarget目標方法
被適配者的方法
複製程式碼

類介面卡模式類圖

這樣我們即可在新介面 Target 中適配舊的介面或類

物件介面卡

物件介面卡與類介面卡不同之處在於,類介面卡通過繼承來完成適配,物件介面卡則是通過關聯來完成,這裡稍微修改一下 Adapter 類即可將轉變為物件介面卡

public class Adapter implements Target{
    // 適配者是物件介面卡的一個屬性
    private Adaptee adaptee = new Adaptee();

    @Override
    public void request() {
        //...
        adaptee.adapteeRequest();
        //...
    }
}
複製程式碼

物件介面卡模式類圖

注意這裡的 Adapter 是將 Adaptee 作為一個成員屬性,而不是繼承它

電壓介面卡

再來一個好理解的例子,我們國家的民用電都是 220V,日本是 110V,而我們的手機充電一般需要 5V,這時候要充電,就需要一個電壓介面卡,將 220V 或者 100V 的輸入電壓變換為 5V 輸出

定義輸出交流電介面,輸出220V交流電類和輸出110V交流電類

public interface AC {
    int outputAC();
}

public class AC110 implements AC {
    public final int output = 110;

    @Override
    public int outputAC() {
        return output;
    }
}

public class AC220 implements AC {
    public final int output = 220;

    @Override
    public int outputAC() {
        return output;
    }
}
複製程式碼

介面卡介面,其中 support() 方法用於檢查輸入的電壓是否與介面卡匹配,outputDC5V() 方法則用於將輸入的電壓變換為 5V 後輸出

public interface DC5Adapter {
    boolean support(AC ac);

    int outputDC5V(AC ac);
}
複製程式碼

實現中國變壓介面卡和日本變壓介面卡

public class ChinaPowerAdapter implements DC5Adapter {
    public static final int voltage = 220;
    
    @Override
    public boolean support(AC ac) {
        return (voltage == ac.outputAC());
    }
    
    @Override
    public int outputDC5V(AC ac) {
        int adapterInput = ac.outputAC();
        //變壓器...
        int adapterOutput = adapterInput / 44;
        System.out.println("使用ChinaPowerAdapter變壓介面卡,輸入AC:" + adapterInput + "V" + ",輸出DC:" + adapterOutput + "V");
        return adapterOutput;
    }
}

public class JapanPowerAdapter implements DC5Adapter {
    public static final int voltage = 110;

    @Override
    public boolean support(AC ac) {
        return (voltage == ac.outputAC());
    }

    @Override
    public int outputDC5V(AC ac) {
        int adapterInput = ac.outputAC();
        //變壓器...
        int adapterOutput = adapterInput / 22;
        System.out.println("使用JapanPowerAdapter變壓介面卡,輸入AC:" + adapterInput + "V" + ",輸出DC:" + adapterOutput + "V");
        return adapterOutput;
    }
}
複製程式碼

測試,準備中國變壓介面卡和日本變壓介面卡各一個,定義一個方法可以根據電壓找到合適的變壓器,然後進行測試

public class Test {
    private List<DC5Adapter> adapters = new LinkedList<DC5Adapter>();

    public Test() {
        this.adapters.add(new ChinaPowerAdapter());
        this.adapters.add(new JapanPowerAdapter());
    }

    // 根據電壓找合適的變壓器
    public DC5Adapter getPowerAdapter(AC ac) {
        DC5Adapter adapter = null;
        for (DC5Adapter ad : this.adapters) {
            if (ad.support(ac)) {
                adapter = ad;
                break;
            }
        }
        if (adapter == null){
            throw new  IllegalArgumentException("沒有找到合適的變壓介面卡");
        }
        return adapter;
    }

    public static void main(String[] args) {
        Test test = new Test();
        AC chinaAC = new AC220();
        DC5Adapter adapter = test.getPowerAdapter(chinaAC);
        adapter.outputDC5V(chinaAC);

        // 去日本旅遊,電壓是 110V
        AC japanAC = new AC110();
        adapter = test.getPowerAdapter(japanAC);
        adapter.outputDC5V(japanAC);
    }
}
複製程式碼

輸出

使用ChinaPowerAdapter變壓介面卡,輸入AC:220V,輸出DC:5V
使用JapanPowerAdapter變壓介面卡,輸入AC:110V,輸出DC:5V
複製程式碼

介面卡模式總結

主要優點

  1. 將目標類和適配者類解耦,通過引入一個介面卡類來重用現有的適配者類,無須修改原有結構。
  2. 增加了類的透明性和複用性,將具體的業務實現過程封裝在適配者類中,對於客戶端類而言是透明的,而且提高了適配者的複用性,同一個適配者類可以在多個不同的系統中複用。
  3. 靈活性和擴充套件性都非常好,通過使用配置檔案,可以很方便地更換介面卡,也可以在不修改原有程式碼的基礎上增加新的介面卡類,完全符合“開閉原則”。

具體來說,類介面卡模式還有如下優點:

  • 由於介面卡類是適配者類的子類,因此可以在介面卡類中置換一些適配者的方法,使得介面卡的靈活性更強。

物件介面卡模式還有如下優點:

  • 一個物件介面卡可以把多個不同的適配者適配到同一個目標;
  • 可以適配一個適配者的子類,由於介面卡和適配者之間是關聯關係,根據“里氏代換原則”,適配者的子類也可通過該介面卡進行適配。

類介面卡模式的缺點如下:

  1. 對於Java、C#等不支援多重類繼承的語言,一次最多隻能適配一個適配者類,不能同時適配多個適配者;
  2. 適配者類不能為最終類,如在Java中不能為final類,C#中不能為sealed類;
  3. 在Java、C#等語言中,類介面卡模式中的目標抽象類只能為介面,不能為類,其使用有一定的侷限性。

物件介面卡模式的缺點如下:

  • 與類介面卡模式相比,要在介面卡中置換適配者類的某些方法比較麻煩。如果一定要置換掉適配者類的一個或多個方法,可以先做一個適配者類的子類,將適配者類的方法置換掉,然後再把適配者類的子類當做真正的適配者進行適配,實現過程較為複雜。

適用場景

  • 系統需要使用一些現有的類,而這些類的介面(如方法名)不符合系統的需要,甚至沒有這些類的原始碼。
  • 想建立一個可以重複使用的類,用於與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起工作。

原始碼分析介面卡模式的典型應用

spring AOP中的介面卡模式

在Spring的Aop中,使用的 Advice(通知) 來增強被代理類的功能。

Advice的型別有:MethodBeforeAdviceAfterReturningAdviceThrowsAdvice

在每個型別 Advice 都有對應的攔截器,MethodBeforeAdviceInterceptorAfterReturningAdviceInterceptorThrowsAdviceInterceptor

Spring需要將每個 Advice 都封裝成對應的攔截器型別,返回給容器,所以需要使用介面卡模式對 Advice 進行轉換

三個適配者類 Adaptee 如下:

public interface MethodBeforeAdvice extends BeforeAdvice {
    void before(Method var1, Object[] var2, @Nullable Object var3) throws Throwable;
}

public interface AfterReturningAdvice extends AfterAdvice {
    void afterReturning(@Nullable Object var1, Method var2, Object[] var3, @Nullable Object var4) throws Throwable;
}

public interface ThrowsAdvice extends AfterAdvice {
}
複製程式碼

目標介面 Target,有兩個方法,一個判斷 Advice 型別是否匹配,一個是工廠方法,建立對應型別的 Advice 對應的攔截器

public interface AdvisorAdapter {
    boolean supportsAdvice(Advice var1);

    MethodInterceptor getInterceptor(Advisor var1);
}
複製程式碼

三個介面卡類 Adapter 分別如下,注意其中的 Advice、Adapter、Interceptor之間的對應關係

class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {
	@Override
	public boolean supportsAdvice(Advice advice) {
		return (advice instanceof MethodBeforeAdvice);
	}

	@Override
	public MethodInterceptor getInterceptor(Advisor advisor) {
		MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice();
		return new MethodBeforeAdviceInterceptor(advice);
	}
}

@SuppressWarnings("serial")
class AfterReturningAdviceAdapter implements AdvisorAdapter, Serializable {
	@Override
	public boolean supportsAdvice(Advice advice) {
		return (advice instanceof AfterReturningAdvice);
	}
	@Override
	public MethodInterceptor getInterceptor(Advisor advisor) {
		AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice();
		return new AfterReturningAdviceInterceptor(advice);
	}
}

class ThrowsAdviceAdapter implements AdvisorAdapter, Serializable {
	@Override
	public boolean supportsAdvice(Advice advice) {
		return (advice instanceof ThrowsAdvice);
	}
	@Override
	public MethodInterceptor getInterceptor(Advisor advisor) {
		return new ThrowsAdviceInterceptor(advisor.getAdvice());
	}
}
複製程式碼

客戶端 DefaultAdvisorAdapterRegistry

public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable {
    private final List<AdvisorAdapter> adapters = new ArrayList(3);

    public DefaultAdvisorAdapterRegistry() {
        // 這裡註冊了介面卡
        this.registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
        this.registerAdvisorAdapter(new AfterReturningAdviceAdapter());
        this.registerAdvisorAdapter(new ThrowsAdviceAdapter());
    }
    
    public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
        List<MethodInterceptor> interceptors = new ArrayList(3);
        Advice advice = advisor.getAdvice();
        if (advice instanceof MethodInterceptor) {
            interceptors.add((MethodInterceptor)advice);
        }

        Iterator var4 = this.adapters.iterator();

        while(var4.hasNext()) {
            AdvisorAdapter adapter = (AdvisorAdapter)var4.next();
            if (adapter.supportsAdvice(advice)) {   // 這裡呼叫介面卡方法
                interceptors.add(adapter.getInterceptor(advisor));  // 這裡呼叫介面卡方法
            }
        }

        if (interceptors.isEmpty()) {
            throw new UnknownAdviceTypeException(advisor.getAdvice());
        } else {
            return (MethodInterceptor[])interceptors.toArray(new MethodInterceptor[0]);
        }
    }
    // ...省略...
}    
複製程式碼

這裡看 while 迴圈裡,逐個取出註冊的介面卡,呼叫 supportsAdvice() 方法來判斷 Advice 對應的型別,然後呼叫 getInterceptor() 建立對應型別的攔截器

spring aop 介面卡模式

這裡應該屬於物件介面卡模式,關鍵字 instanceof 可看成是 Advice 的方法,不過這裡的 Advice 物件是從外部傳進來,而不是成員屬性

spring JPA中的介面卡模式

在Spring的ORM包中,對於JPA的支援也是採用了介面卡模式,首先定義了一個介面的 JpaVendorAdapter,然後不同的持久層框架都實現此介面。

jpaVendorAdapter:用於設定實現廠商JPA實現的特定屬性,如設定Hibernate的是否自動生成DDL的屬性generateDdl;這些屬性是廠商特定的,因此最好在這裡設定;目前Spring提供 HibernateJpaVendorAdapterOpenJpaVendorAdapterEclipseLinkJpaVendorAdapterTopLinkJpaVendorAdapter 四個實現。其中最重要的屬性是 database,用來指定使用的資料庫型別,從而能根據資料庫型別來決定比如如何將資料庫特定異常轉換為Spring的一致性異常,目前支援如下資料庫(DB2、DERBY、H2、HSQL、INFORMIX、MYSQL、ORACLE、POSTGRESQL、SQL_SERVER、SYBASE)

public interface JpaVendorAdapter
{
  // 返回一個具體的持久層提供者
  public abstract PersistenceProvider getPersistenceProvider();

  // 返回持久層提供者的包名
  public abstract String getPersistenceProviderRootPackage();

  // 返回持久層提供者的屬性
  public abstract Map<String, ?> getJpaPropertyMap();

  // 返回JpaDialect
  public abstract JpaDialect getJpaDialect();

  // 返回持久層管理器工廠
  public abstract Class<? extends EntityManagerFactory> getEntityManagerFactoryInterface();

  // 返回持久層管理器
  public abstract Class<? extends EntityManager> getEntityManagerInterface();

  // 自定義回撥方法
  public abstract void postProcessEntityManagerFactory(EntityManagerFactory paramEntityManagerFactory);
}
複製程式碼

我們來看其中一個介面卡實現類 HibernateJpaVendorAdapter

public class HibernateJpaVendorAdapter extends AbstractJpaVendorAdapter {
    //設定持久層提供者
    private final PersistenceProvider persistenceProvider;
    //設定持久層方言
    private final JpaDialect jpaDialect;

    public HibernateJpaVendorAdapter() {
        this.persistenceProvider = new HibernatePersistence();
        this.jpaDialect = new HibernateJpaDialect();
    }

    //返回持久層方言
    public PersistenceProvider getPersistenceProvider() {
        return this.persistenceProvider;
    }

    //返回持久層提供者
    public String getPersistenceProviderRootPackage() {
        return "org.hibernate";
    }

    //返回JPA的屬性
    public Map<String, Object> getJpaPropertyMap() {
        Map jpaProperties = new HashMap();

        if (getDatabasePlatform() != null) {
            jpaProperties.put("hibernate.dialect", getDatabasePlatform());
        } else if (getDatabase() != null) {
            Class databaseDialectClass = determineDatabaseDialectClass(getDatabase());
            if (databaseDialectClass != null) {
                jpaProperties.put("hibernate.dialect",
                        databaseDialectClass.getName());
            }
        }

        if (isGenerateDdl()) {
            jpaProperties.put("hibernate.hbm2ddl.auto", "update");
        }
        if (isShowSql()) {
            jpaProperties.put("hibernate.show_sql", "true");
        }

        return jpaProperties;
    }

    //設定資料庫
    protected Class determineDatabaseDialectClass(Database database)     
    {                                                                                       
        switch (1.$SwitchMap$org$springframework$orm$jpa$vendor$Database[database.ordinal()]) 
        {                                                                                     
        case 1:                                                                             
          return DB2Dialect.class;                                                            
        case 2:                                                                               
          return DerbyDialect.class;                                                          
        case 3:                                                                               
          return H2Dialect.class;                                                             
        case 4:                                                                               
          return HSQLDialect.class;                                                           
        case 5:                                                                               
          return InformixDialect.class;                                                       
        case 6:                                                                               
          return MySQLDialect.class;                                                          
        case 7:                                                                               
          return Oracle9iDialect.class;                                                       
        case 8:                                                                               
          return PostgreSQLDialect.class;                                                     
        case 9:                                                                               
          return SQLServerDialect.class;                                                      
        case 10:                                                                              
          return SybaseDialect.class; }                                                       
        return null;              
    }

    //返回JPA方言
    public JpaDialect getJpaDialect() {
        return this.jpaDialect;
    }

    //返回JPA實體管理器工廠
    public Class<? extends EntityManagerFactory> getEntityManagerFactoryInterface() {
        return HibernateEntityManagerFactory.class;
    }

    //返回JPA實體管理器
    public Class<? extends EntityManager> getEntityManagerInterface() {
        return HibernateEntityManager.class;
    }
}
複製程式碼

配置檔案中可以這樣指定

<bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> 
   <property name="generateDdl" value="false" />  
   <property name="database" value="HSQL"/>  
</bean>  
<bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>  
複製程式碼

spring MVC中的介面卡模式

Spring MVC中的介面卡模式主要用於執行目標 Controller 中的請求處理方法。

在Spring MVC中,DispatcherServlet 作為使用者,HandlerAdapter 作為期望介面,具體的介面卡實現類用於對目標類進行適配,Controller 作為需要適配的類。

為什麼要在 Spring MVC 中使用介面卡模式?Spring MVC 中的 Controller 種類眾多,不同型別的 Controller 通過不同的方法來對請求進行處理。如果不利用介面卡模式的話,DispatcherServlet 直接獲取對應型別的 Controller,需要的自行來判斷,像下面這段程式碼一樣:

if(mappedHandler.getHandler() instanceof MultiActionController){  
   ((MultiActionController)mappedHandler.getHandler()).xxx  
}else if(mappedHandler.getHandler() instanceof XXX){  
    ...  
}else if(...){  
   ...  
}  
複製程式碼

這樣假設如果我們增加一個 HardController,就要在程式碼中加入一行 if(mappedHandler.getHandler() instanceof HardController),這種形式就使得程式難以維護,也違反了設計模式中的開閉原則 – 對擴充套件開放,對修改關閉。

我們來看看原始碼,首先是介面卡介面 HandlerAdapter

public interface HandlerAdapter {
    boolean supports(Object var1);

    ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;

    long getLastModified(HttpServletRequest var1, Object var2);
}
複製程式碼

現該介面的介面卡每一個 Controller 都有一個介面卡與之對應,這樣的話,每自定義一個 Controller 需要定義一個實現 HandlerAdapter 的介面卡。

springmvc 中提供的 Controller 實現類有如下

spring mvc Controller 提供的實現類

springmvc 中提供的 HandlerAdapter 實現類如下

spring mvc HandlerAdapter 提供的實現類

HttpRequestHandlerAdapter 這個介面卡程式碼如下

public class HttpRequestHandlerAdapter implements HandlerAdapter {
    public HttpRequestHandlerAdapter() {
    }

    public boolean supports(Object handler) {
        return handler instanceof HttpRequestHandler;
    }

    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ((HttpRequestHandler)handler).handleRequest(request, response);
        return null;
    }

    public long getLastModified(HttpServletRequest request, Object handler) {
        return handler instanceof LastModified ? ((LastModified)handler).getLastModified(request) : -1L;
    }
}
複製程式碼

當Spring容器啟動後,會將所有定義好的介面卡物件存放在一個List集合中,當一個請求來臨時,DispatcherServlet 會通過 handler 的型別找到對應介面卡,並將該介面卡物件返回給使用者,然後就可以統一通過介面卡的 hanle() 方法來呼叫 Controller 中的用於處理請求的方法。

public class DispatcherServlet extends FrameworkServlet {
    private List<HandlerAdapter> handlerAdapters;
    
    //初始化handlerAdapters
    private void initHandlerAdapters(ApplicationContext context) {
        //..省略...
    }
    
    // 遍歷所有的 HandlerAdapters,通過 supports 判斷找到匹配的介面卡
    protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
		for (HandlerAdapter ha : this.handlerAdapters) {
			if (logger.isTraceEnabled()) {
				logger.trace("Testing handler adapter [" + ha + "]");
			}
			if (ha.supports(handler)) {
				return ha;
			}
		}
	}
	
	// 分發請求,請求需要找到匹配的介面卡來處理
	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;

		// Determine handler for the current request.
		mappedHandler = getHandler(processedRequest);
			
		// 確定當前請求的匹配的介面卡.
		HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

		ha.getLastModified(request, mappedHandler.getHandler());
					
		mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    }
	// ...省略...
}	
複製程式碼

通過介面卡模式我們將所有的 controller 統一交給 HandlerAdapter 處理,免去了寫大量的 if-else 語句對 Controller 進行判斷,也更利於擴充套件新的 Controller 型別。

參考:
劉偉:設計模式Java版
慕課網java設計模式精講 Debug 方式+記憶體分析
孤落:Spring MVC中的介面卡模式
ToughMind_:深入淺出設計模式(五):7.介面卡模式

相關文章