結構性設計模式(四)介面卡模式【如何把方的變成圓的】

30.DCMA發表於2020-09-26

強扭的瓜,湊合著用

一、概念及實現

1.1 概念

介面卡模式的英文翻譯是 Adapter Design Pattern。顧名思義,這個模式就是用來做適配的,它將不相容的介面轉換為可相容的介面,讓原本由於介面不相容而不能一起工作的類可以一起工作。把兩種不相容的介面,通過轉接變得可以一起工作。

介面卡模式有兩種實現方式:類介面卡和物件介面卡。其中,類介面卡使用繼承關係來實現,物件介面卡使用組合關係來實現。

具體的程式碼實現如下所示。其中,ITarget 表示要轉化成的介面定義。Adaptee 是一組不相容 ITarget 介面定義的介面,Adaptor 將 Adaptee 轉化成一組符合 ITarget 介面定義的介面。

1.2 基於繼承的實現和基於組合的實現

  • 基於繼承的實現

// 類介面卡: 基於繼承
public interface ITarget {
  void f1();
  void f2();
  void fc();
}

public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}

public class Adaptor extends Adaptee implements ITarget {
  public void f1() {
    super.fa();
  }
  
  public void f2() {
    //...重新實現f2()...
  }
  
  // 這裡fc()不需要實現,直接繼承自Adaptee,這是跟物件介面卡最大的不同點
}
  • 基於組合的實現
// 物件介面卡:基於組合
public interface ITarget {
  void f1();
  void f2();
  void fc();
}

public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}

public class Adaptor implements ITarget {
  private Adaptee adaptee;
  
  public Adaptor(Adaptee adaptee) {
    this.adaptee = adaptee;
  }
  
  public void f1() {
    adaptee.fa(); //委託給Adaptee
  }
  
  public void f2() {
    //...重新實現f2()...
  }
  
  public void fc() {
    adaptee.fc();
  }
}

1.3 使用哪一種

判斷的標準主要有兩個,一個是 Adaptee 介面的個數,另一個是 Adaptee 和 ITarget 的契合程度。

  • 如果 Adaptee 介面並不多,那兩種實現方式都可以。
  • 如果 Adaptee 介面很多,而且 Adaptee 和 ITarget 介面定義大部分都相同,那我們推薦使用類介面卡,因為 Adaptor 複用父類 Adaptee 的介面,比起物件介面卡的實現方式,Adaptor 的程式碼量要少一些。
  • 如果 Adaptee 介面很多,而且 Adaptee 和 ITarget 介面定義大部分都不相同,那我們推薦使用物件介面卡,因為組合結構相對於繼承更加靈活。

二、適用的場景

介面卡模式可以看作一種“補償模式”,用來補救設計上的缺陷。應用這種模式算是“無奈之舉”。如果在設計初期,我們就能協調規避介面不相容的問題,那這種模式就沒有應用的機會了。

2.1 封裝有缺陷的介面設計

假設我們依賴的外部系統在介面設計方面有缺陷(比如包含大量靜態方法),引入之後會影響到我們自身程式碼的可測試性。為了隔離設計上的缺陷,我們希望對外部系統提供的介面進行二次封裝,抽象出更好的介面設計,這個時候就可以使用介面卡模式了。

舉個例子來解釋一下,你直接看程式碼應該會更清晰。具體程式碼如下所示:


public class CD { //這個類來自外部sdk,我們無權修改它的程式碼
  //...
  public static void staticFunction1() { //... }
  
  public void uglyNamingFunction2() { //... }

  public void tooManyParamsFunction3(int paramA, int paramB, ...) { //... }
  
   public void lowPerformanceFunction4() { //... }
}

// 使用介面卡模式進行重構
public class ITarget {
  void function1();
  void function2();
  void fucntion3(ParamsWrapperDefinition paramsWrapper);
  void function4();
  //...
}
// 注意:介面卡類的命名不一定非得末尾帶Adaptor
public class CDAdaptor extends CD implements ITarget {
  //...
  public void function1() {
     super.staticFunction1();
  }
  
  public void function2() {
    super.uglyNamingFucntion2();
  }
  
  public void function3(ParamsWrapperDefinition paramsWrapper) {
     super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...);
  }
  
  public void function4() {
    //...reimplement it...
  }
}

2.2 統一多個類的介面設計

某個功能的實現依賴多個外部系統(或者說類)。通過介面卡模式,將它們的介面適配為統一的介面定義,然後我們就可以使用多型的特性來複用程式碼邏輯。具體我還是舉個例子來解釋一下。

假設我們的系統要對使用者輸入的文字內容做敏感詞過濾,為了提高過濾的召回率,我們引入了多款第三方敏感詞過濾系統,依次對使用者輸入的內容進行過濾,過濾掉儘可能多的敏感詞。但是,每個系統提供的過濾介面都是不同的。這就意味著我們沒法複用一套邏輯來呼叫各個系統。這個時候,我們就可以使用介面卡模式,將所有系統的介面適配為統一的介面定義,這樣我們可以複用呼叫敏感詞過濾的程式碼。你可以配合著下面的程式碼示例,來理解我剛才舉的這個例


public class ASensitiveWordsFilter { // A敏感詞過濾系統提供的介面
  //text是原始文字,函式輸出用***替換敏感詞之後的文字
  public String filterSexyWords(String text) {
    // ...
  }
  
  public String filterPoliticalWords(String text) {
    // ...
  } 
}

public class BSensitiveWordsFilter  { // B敏感詞過濾系統提供的介面
  public String filter(String text) {
    //...
  }
}

public class CSensitiveWordsFilter { // C敏感詞過濾系統提供的介面
  public String filter(String text, String mask) {
    //...
  }
}

// 未使用介面卡模式之前的程式碼:程式碼的可測試性、擴充套件性不好
public class RiskManagement {
  private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
  private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
  private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
  
  public String filterSensitiveWords(String text) {
    String maskedText = aFilter.filterSexyWords(text);
    maskedText = aFilter.filterPoliticalWords(maskedText);
    maskedText = bFilter.filter(maskedText);
    maskedText = cFilter.filter(maskedText, "***");
    return maskedText;
  }
}

// 使用介面卡模式進行改造
public interface ISensitiveWordsFilter { // 統一介面定義
  String filter(String text);
}

public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
  private ASensitiveWordsFilter aFilter;
  public String filter(String text) {
    String maskedText = aFilter.filterSexyWords(text);
    maskedText = aFilter.filterPoliticalWords(maskedText);
    return maskedText;
  }
}
//...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor...

// 擴充套件性更好,更加符合開閉原則,如果新增一個新的敏感詞過濾系統,
// 這個類完全不需要改動;而且基於介面而非實現程式設計,程式碼的可測試性更好。
public class RiskManagement { 
  private List<ISensitiveWordsFilter> filters = new ArrayList<>();
 
  public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
    filters.add(filter);
  }
  
  public String filterSensitiveWords(String text) {
    String maskedText = text;
    for (ISensitiveWordsFilter filter : filters) {
      maskedText = filter.filter(maskedText);
    }
    return maskedText;
  }
}

2.3 替換依賴的外部系統

當我們把專案中依賴的一個外部系統替換為另一個外部系統的時候,利用介面卡模式,可以減少對程式碼的改動。具體的程式碼示例如下所示:


// 外部系統A
public interface IA {
  //...
  void fa();
}
public class A implements IA {
  //...
  public void fa() { //... }
}
// 在我們的專案中,外部系統A的使用示例
public class Demo {
  private IA a;
  public Demo(IA a) {
    this.a = a;
  }
  //...
}
Demo d = new Demo(new A());

// 將外部系統A替換成外部系統B
public class BAdaptor implemnts IA {
  private B b;
  public BAdaptor(B b) {
    this.b= b;
  }
  public void fa() {
    //...
    b.fb();
  }
}
// 藉助BAdaptor,Demo的程式碼中,呼叫IA介面的地方都無需改動,
// 只需要將BAdaptor如下注入到Demo即可。
Demo d = new Demo(new BAdaptor(new B()));

2.4 相容老版本介面

在做版本升級的時候,對於一些要廢棄的介面,我們不直接將其刪除,而是暫時保留,並且標註為 deprecated,並將內部實現邏輯委託為新的介面實現。這樣做的好處是,讓使用它的專案有個過渡期,而不是強制進行程式碼修改。這也可以粗略地看作介面卡模式的一個應用場景。同樣,我還是通過一個例子,來進一步解釋一下。

JDK1.0 中包含一個遍歷集合容器的類 Enumeration。JDK2.0 對這個類進行了重構,將它改名為 Iterator 類,並且對它的程式碼實現做了優化。但是考慮到如果將 Enumeration 直接從 JDK2.0 中刪除,那使用 JDK1.0 的專案如果切換到 JDK2.0,程式碼就會編譯不通過。為了避免這種情況的發生,我們必須把專案中所有使用到 Enumeration 的地方,都修改為使用 Iterator 才行。

單獨一個專案做 Enumeration 到 Iterator 的替換,勉強還能接受。但是,使用 Java 開發的專案太多了,一次 JDK 的升級,導致所有的專案不做程式碼修改就會編譯報錯,這顯然是不合理的。這就是我們經常所說的不相容升級。為了做到相容使用低版本 JDK 的老程式碼,我們可以暫時保留 Enumeration 類,並將其實現替換為直接呼叫 Itertor。程式碼示例如下所示:


public class Collections {
  public static Emueration emumeration(final Collection c) {
    return new Enumeration() {
      Iterator i = c.iterator();
      
      public boolean hasMoreElments() {
        return i.hashNext();
      }
      
      public Object nextElement() {
        return i.next():
      }
    }
  }
}

三、主要參考內容

相關文章