07.介面卡模式設計思想

杨充發表於2024-10-31

07.介面卡模式設計思想

目錄介紹
  • 01.介面卡模式基礎

    • 1.1 介面卡模式由來
    • 1.2 介面卡模式定義
    • 1.3 介面卡模式場景
    • 1.4 介面卡模式思考
  • 02.介面卡模式實現

    • 2.1 羅列一個場景
    • 2.2 用例子理解介面卡
    • 2.3 介面卡基本實現
    • 2.4 如何選擇介面卡
  • 03.介面卡模式分析

    • 3.1 類介面卡案例
    • 3.2 物件介面卡案例
    • 3.3 介面卡模式結構圖
    • 3.4 介面卡模式時序圖
  • 04.介面卡應用解析

    • 4.1 讀卡器適配案例【類】
    • 4.2 類介面卡優缺點
    • 4.3 播放器介面卡案例【物件】
    • 4.4 物件介面卡優缺點
  • 05.實際場景演變

    • 5.1 封裝有缺陷的介面設計
    • 5.2 設計統一多個類的介面
    • 5.3 替換依賴的外部系統
    • 5.4 相容老版本介面
    • 5.5 適配不同格式的資料
    • 5.6 Java介面卡模式使用
    • 5.7 資料庫介面卡
    • 5.8 日誌框架介面卡
    • 5.9 第三方庫介面卡
  • 06.介面卡總結一下

    • 6.1 介面卡使用環境
    • 6.2 介面卡模式擴充
    • 6.3 跟其他模式對比
  • 07.動態代理總結

    • 7.1 總結一下學習
    • 7.2 更多內容推薦

推薦一個好玩網站

一個最純粹的技術分享網站,打造精品技術程式設計專欄!程式設計進階網

https://yccoding.com/

01.介面卡模式基礎

1.0 本部落格AI摘要

介面卡模式是一種結構型設計模式,用於將不相容的介面轉換為可相容的介面,使原本不能一起工作的類可以協同工作。本文詳細介紹了介面卡模式的基礎、實現方式(類介面卡和物件介面卡)、應用場景(如封裝有缺陷的介面、統一多個類的介面、替換依賴的外部系統等)以及優缺點。透過具體案例(如讀卡器適配、播放器適配)和實際開發中的應用(如資料庫介面卡、日誌框架介面卡),幫助讀者深入理解和應用介面卡模式。

1.1 介面卡模式由來

介面卡模式的英文翻譯是 Adapter Design Pattern。

顧名思義,這個模式就是用來做適配的,它將不相容的介面轉換為可相容的介面,讓原本由於介面不相容而不能一起工作的類可以一起工作。

對於這個模式,有一個經常被拿來解釋它的例子,就是 USB 轉接頭充當介面卡,把兩種不相容的介面,透過轉接變得可以一起工作。

主要解決問題:主要解決在軟體系統中,常常要將一些"現存的物件"放到新的環境中,而新環境要求的介面是現物件不能滿足的。

簡單來說,用於事後補救措施,專案程式碼後期,想讓不想關的類,變成可以一起工作

1.2 介面卡模式定義

介面卡模式(Adapter Pattern)的定義 :

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

1.3 介面卡模式場景

在計算機程式設計中,當我們有兩個已有的功能,但由於某種原因它們不能直接協同工作時,我們使用介面卡模式。

舉個例子1:類似USB轉接頭,既可以連結Android手機,也可以連結IOS手機。

舉個例子2:歐洲製造的電器,但你住在中國。你不能直接將這部電器插入牆上,因為插頭和插座不相容。此時,你需要一個介面卡,將歐洲插頭轉換為中國插座。

1.4 介面卡模式思考

介面卡模式應該在真正需要時使用,而不是為了強行適配而使用。

在設計階段,應該儘量考慮介面的一致性和相容性,以減少對介面卡模式的依賴。

02.介面卡模式原理與實現

2.1 羅列一個場景

介面卡模式在軟體開發中有多種應用場景,以下是一些常見的應用場景:

  1. 資料庫訪問:介面卡模式可以用於將不同資料庫供應商的API轉換為統一的資料庫訪問介面,以便在不同的資料庫之間切換和使用。
  2. 舊系統升級:當需要將舊系統升級為新系統時,介面卡模式可以用於保留舊系統的功能,並將其介面轉換為新系統所需的介面。
  3. 第三方庫使用:當使用第三方庫或元件時,其提供的介面可能與當前系統的介面不相容。介面卡模式可以用於將第三方庫的介面轉換為系統所需的介面。

2.2 用例子理解介面卡

介面卡有兩種方式:

  1. 類的介面卡。透過繼承來實現
  2. 物件的介面卡。透過組合來實現

具體的程式碼實現如下所示。

其中,ITarget 表示要轉化成的介面定義。Adaptee 是一組不相容 ITarget 介面定義的介面,Adaptor 將 Adaptee 轉化成一組符合 ITarget 介面定義的介面。

2.3 介面卡模式基本實現

類的介面卡。透過繼承來實現

●目標(Target)角色:這就是所期待得到的介面。注意:由於這裡討論的是類介面卡模式,因此目標不可以是類。

●源(Adapee)角色:現在需要適配的介面。

●介面卡(Adaper)角色:介面卡類是本模式的核心。介面卡把源介面轉換成目標介面。顯然,這一角色不可以是介面,而必須是具體類。

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

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

    public void fb() {
        //...
    }

    public void fc() {
        //...
    }
}

public class Adaptor extends Adaptee implements ITarget {
    public void f1() {
        super.fa();
        System.out.println("呼叫介面卡中方法");
    }

    public void f2() {
        //...重新實現f2()...
        super.fb();
        System.out.println("重新實現f2");
    }

    // 這裡fc()不需要實現,直接繼承自Adaptee,這是跟物件介面卡最大的不同點
    @Override
    public void f3() {

    }
}

物件的介面卡。透過組合來實現

// 物件介面卡:基於組合
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 final Adaptee adaptee;

    public Adaptor(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    public void f1() {
        adaptee.fa(); //委託給Adaptee
    }

    public void f2() {
        //...重新實現f2()...
    }

    public void fc() {
        adaptee.fc();
    }
}

2.4 如何選擇介面卡

針對這兩種實現方式,在實際的開發中,到底該如何選擇使用哪一種呢?

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

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

03.介面卡模式分析

3.1 類介面卡案例

類介面卡是一種結構型設計模式,它允許將一個類的介面轉換為另一個客戶端所期望的介面。透過繼承原始類和實現目標介面,類介面卡使得原始類的介面與目標介面相容。

特點: 1.使用繼承:類介面卡透過繼承源類來實現適配功能。2.單一適配:由於 Java 中不支援多重繼承,類介面卡只能適配一個源類。

3.2 物件介面卡案例

物件介面卡是介面卡模式的一種變體,它透過組合(而不是繼承)原始類和目標介面來實現介面卡功能。在物件介面卡中,介面卡類持有一個原始類的例項,並實現目標介面,透過呼叫原始類的方法來實現目標介面的方法。

特點:1.介面卡透過組合原始類的例項來實現介面卡功能,而不是透過繼承原始類。2.物件介面卡可以重用現有的功能,無需修改原始類的程式碼。

3.3 介面卡模式結構圖

介面卡模式包含如下角色:

  1. Target:目標抽象類。定義客戶端所期望的介面。介面卡將原始類的介面轉換為目標介面,使得客戶端可以透過目標介面來訪問原始類的功能。
  2. Adapter:介面卡類。介面卡類實現了目標介面,並持有原始類的例項。介面卡類中的方法透過呼叫原始類的方法來實現目標介面的方法。介面卡充當了目標介面和原始類之間的橋樑。
  3. Adaptee:適配者類【原始類】。需要被適配的類,它定義了不相容目標介面的方法。介面卡模式的目標是使得客戶端可以透過目標介面來使用原始類的功能。
  4. Client:客戶類。客戶端可以透過目標介面來使用原始類的功能,而不需要直接與原始類互動。

3.4 介面卡模式時序圖

介面卡模式時序圖如下所示:

04.介面卡應用解析

4.1 讀卡器適配案例【類】

讀卡器需求分析:

現有一臺電腦只能讀取SD卡,而要讀取TF卡中的內容的話就需要使用到介面卡模式。建立一個讀卡器,將TF卡中的內容讀取出來。

首先我們先開發電腦讀取SD卡的業務:

Computer只能使用SD卡,所以方法名為readSD(),需要SDCard型別的物件。具體做法是:主要是SDCard(包含兩個方法:讀資料,寫資料),子實現類為SDCardImpl。

public interface SDCard {
    //從SD卡中讀取資料
    String readSD();

    //往SD卡中寫資料
    void writeSD(String msg);
}

//具體的SD卡
public class SDCardImpl implements SDCard {
    @Override
    public String readSD() {
        String msg = "SDCard read msg : SD";
        System.out.println(msg);
        return msg;
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("SDCard write msg : " + msg);
    }
}

//計算機類
public class Computer {

    //從SD卡中讀取資料
    public String readSD(SDCard sdCard) {
        if (sdCard == null) {
            throw new NullPointerException("sd card is null");
        }
        return sdCard.readSD();
    }
}

private void test1() {
    //計算機讀SD卡
    Computer computer = new Computer();
    SDCardImpl sdCard = new SDCardImpl();
    computer.readSD(sdCard);
}

//輸出結果是:
//SDCard read msg : SD

然後,思想一下如何給TF做適配。

  1. 第一步:定義了一個介面 TFCard 介面,有兩個方法(讀取資料,寫資料)。子實現類TFCardImpl重寫了介面的兩個方法
  2. 第二步:當我們想用Computer去讀取TF卡中的內容,不能直接讀取,需要定義介面卡類。要讓這個介面卡類實現目標介面,就要重寫SDCard中的兩個方法,同時我們要讓它去繼承TFCardImpl。
  3. 第三步:最後,實現之後這兩個方法看似是從SD卡中讀資料寫資料,但是實際上用的是TF卡中的功能
//適配者類的介面
public interface TFCard {
    //從TF卡中讀取資料
    String readTF();
    //往TF卡中寫資料
    void writeTF(String msg);
}

//適配者類
public class TFCardImpl implements TFCard {

    @Override
    public String readTF() {
        String msg = "TFCard read msg : TF";
        System.out.println(msg);
        return msg;
    }

    @Override
    public void writeTF(String msg) {
        System.out.println("TFCard write msg : " + msg);
    }
}

//介面卡類
public class SDAdapterTF extends TFCardImpl implements SDCard {

    @Override
    public String readSD() {
        System.out.println("adapter read tf card");
        return readTF();
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("adapter wrete tf card");
        writeTF(msg);
    }
}

private void test2() {
    //計算器適配,讀卡器,去讀TF卡中的內容
    Computer computer = new Computer();
    //建立介面卡物件,完全沒有影響之前計算機讀SD卡的邏輯
    SDAdapterTF sdAdapterTF = new SDAdapterTF();
    computer.readSD(sdAdapterTF);
}

透過這個案例可以知道

介面卡模式是一種強大的設計模式,能夠有效解決介面不相容的問題,使得不同介面的類能夠協同工作。透過合理使用介面卡模式,可以提高系統的靈活性和複用性,但也需要注意其可能帶來的複雜性和效能影響。

4.2 類介面卡優缺點

優點

  1. 簡單實現:由於類介面卡透過繼承實現,它可以直接訪問被適配類(Adaptee)的所有方法。這使得實現介面卡時相對簡單,不需要額外的委託邏輯。
  2. 提高程式碼的可複用性:介面卡模式透過繼承實現,使得子類能夠繼承父類的所有功能,從而提高了程式碼的複用性。
  3. 可以重定義被適配類的一些行為:透過繼承,可以在介面卡類中重定義被適配類的一些方法,實現更加靈活的適配。

缺點

  1. 受限於單繼承:類介面卡模式在Java等單繼承語言中有一個顯著的缺點,即一個類只能繼承一個父類。這意味著如果介面卡類已經有一個父類,就不能再使用類介面卡模式來繼承另一個類。
  2. 高耦合:介面卡類和被適配類之間的耦合度較高,因為介面卡類直接繼承了被適配類的實現。如果被適配類發生變化,介面卡類可能也需要進行相應的修改。
  3. 不符合“組合優於繼承”原則:物件導向設計中,組合優於繼承的原則提倡使用組合來代替繼承,以降低類之間的耦合度。類介面卡模式違背了這一原則,因為它是透過繼承來實現適配的。

4.3 播放器介面卡案例【物件】

播放器案例需求分析

想象一下,我們有一個MediaPlayer介面,它可以播放mp3格式的檔案。

private void test1() {
    MediaPlayer mp3Player = new Mp3Player();
    mp3Player.play("mp3","file.mp3");
}

// MediaPlayer.java
public interface MediaPlayer {
    void play(String audioType, String fileName);
}

// Mp3Player.java
public class Mp3Player implements MediaPlayer {
    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file. Name: " + fileName);
        } else {
            System.out.println("Invalid media. " + audioType + " format not supported");
        }
    }
}

現在,我們想擴充套件這個功能,使其也可以播放其他格式的檔案,比如vlc和mp4。但是,我們不希望修改原始的MediaPlayer介面。

為了支援更多的格式,我們將建立一個新的介面AdvancedMediaPlayer:

private void test2() {
    AdvancedMediaPlayer vlcPlayer = new VlcPlayer();
    vlcPlayer.playVlc("vlc");
    AdvancedMediaPlayer mp4Player = new Mp4Player();
    mp4Player.playMp4("mp4");
}

// AdvancedMediaPlayer.java
public interface AdvancedMediaPlayer {
    void playVlc(String fileName);
    void playMp4(String fileName);
}

// VlcPlayer.java
public class VlcPlayer implements AdvancedMediaPlayer {
    @Override
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file. Name: " + fileName);
    }

    @Override
    public void playMp4(String fileName) {
        // Do nothing
    }
}

// Mp4Player.java
public class Mp4Player implements AdvancedMediaPlayer {
    @Override
    public void playVlc(String fileName) {
        // Do nothing
    }

    @Override
    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file. Name: " + fileName);
    }
}

現在,我們已經有了基礎的播放器,但我們還需要一個介面卡,以便MediaPlayer可以使用AdvancedMediaPlayer。

實現MediaPlayer介面卡。為了使MediaPlayer能夠使用AdvancedMediaPlayer,我們需要建立一個介面卡。這個介面卡將決定使用哪個AdvancedMediaPlayer的實現。

public class MediaAdapter implements MediaPlayer {

    AdvancedMediaPlayer advancedMusicPlayer;

    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer = new VlcPlayer();
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer = new Mp4Player();
        }
    }

    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer.playVlc(fileName);
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer.playMp4(fileName);
        }
    }
}

現在,讓我們擴充套件我們的Mp3Player類,使其可以使用MediaAdapter來播放其他格式:

public class AudioPlayer implements MediaPlayer {
    MediaAdapter mediaAdapter;

    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file. Name: " + fileName);
        } else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        } else {
            System.out.println("Invalid media. " + audioType + " format not supported");
        }
    }
}

讓我們透過一些測試來驗證我們的介面卡是否正常工作:

private void test3() {
    AudioPlayer audioPlayer = new AudioPlayer();
    audioPlayer.play("mp3", "mySong.mp3");
    audioPlayer.play("mp4", "video.mp4");
    audioPlayer.play("vlc", "movie.vlc");
    audioPlayer.play("avi", "doubi.avi");  // Not supported
}

介面卡模式是一個非常有用的模式,它允許我們整合不相容的介面,而不需要修改原始程式碼。在本例中,我們成功地擴充套件了MediaPlayer的功能,使其可以播放其他格式的檔案,而不需要改變其原始定義。

像上面的MediaPlayer示例一樣,你可能會在許多真實的應用程式中遇到類似的場景,其中一些舊的介面需要與新的介面一起工作,但又不希望進行大規模的重寫。

4.4 物件介面卡優缺點

物件介面卡模式具有以下優點:

  1. 介面卡可以重用現有的功能,無需修改原始類的程式碼。介面卡透過將原始類的介面轉換為目標介面,使得客戶端可以無縫地使用原始類的功能。
  2. 物件介面卡使用組合來實現介面卡功能,因此可以適配原始類的子類。這使得物件介面卡具有更大的靈活性和可擴充套件性。
  3. 物件介面卡可以在不修改客戶端程式碼的情況下實現介面轉換。客戶端只需要透過呼叫介面卡類的方法來使用原始類的功能,而不需要直接與原始類互動。
  4. 物件介面卡可以適配多個不同的原始類,只需建立相應的介面卡類即可。這使得物件介面卡具有更高的複用性和可維護性。

然而,物件介面卡模式也有一些缺點和限制:

  1. 物件介面卡引入了額外的物件引用,可能會增加記憶體消耗。每個介面卡例項都需要持有一個原始類的例項。
  2. 物件介面卡在介面卡類中引入了原始類的功能,可能會導致介面的冗餘和複雜性增加。
  3. 物件介面卡需要透過組合來實現介面卡功能,這可能需要更多的程式碼和配置。

05.應用場景分析

大概有哪些場景

  1. 封裝有缺陷的介面設計
  2. 統一多個類的介面設計

為何用這個

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

5.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...
  }
}

5.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;
  }
}

5.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()));

5.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():
      }
    }
  }
}

5.5 適配不同格式的資料

前面我們講到,介面卡模式主要用於介面的適配,實際上,它還可以用在不同格式的資料之間的適配。

比如,把從不同徵信系統拉取的不同格式的徵信資料,統一為相同的格式,以方便儲存和使用。

再比如,Java 中的 Arrays.asList() 也可以看作一種資料介面卡,將陣列型別的資料轉化為集合容器型別。

List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");

5.6 Java介面卡模式使用

JDK1.1 之前提供的容器有 Arrays,Vector,Stack,Hashtable,Properties,BitSet,其中定義了一種訪問群集內各元素的標準方式,稱為 Enumeration(列舉器)介面。

Vector v=new Vector();
for (Enumeration enum =v.elements(); enum.hasMoreElements();) {
  Object o = enum.nextElement();
  processObject(o);
}

JDK1.2 版本中引入了 Iterator 介面,新版本的集合對(HashSet,HashMap,WeakHashMap,ArrayList,TreeSet,TreeMap, LinkedList)是透過 Iterator 介面訪問集合元素。

List list=new ArrayList();
for(Iterator it=list.iterator();it.hasNext();){
   System.out.println(it.next());
}

這樣,如果將老版本的程式執行在新的 Java 編譯器上就會出錯。因為 List 介面中已經沒有 elements(),而只有 iterator() 了。

那麼如何將老版本的程式執行在新的 Java 編譯器上呢? 如果不加修改,是肯定不行的,但是修改要遵循“開-閉”原則。我們可以用 Java 設計模式中的介面卡模式解決這個問題。

public class NewEnumeration implements Enumeration {
    Iterator it;
  
    public NewEnumeration(Iterator it) {
        this.it = it;
    }
  
    public boolean hasMoreElements() {
        return it.hasNext();
    }
  
    public Object nextElement() {
        return it.next();
    }
  
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("a");
        list.add("b");
        list.add("C");
        for (Enumeration e = new NewEnumeration(list.iterator()); e.hasMoreElements(); ) {
            System.out.println(e.nextElement());
        }
    }
}

NewEnumeration 是一個介面卡類,透過它實現了從 Iterator 介面到 Enumeration 介面的適配,這樣我們就可以使用老版本的程式碼來使用新的集合物件了。

5.7 資料庫介面卡

在許多應用程式中,需要與多種資料庫進行互動。如果每種資料庫都有不同的介面,那麼使用介面卡模式可以簡化程式碼,並提高程式碼的可維護性和可擴充套件性。例如:

public interface Database {
    void connect();
    void query(String sql);
}

public class MySqlDatabase implements Database {
    @Override
    public void connect() {
        System.out.println("Connecting to MySQL Database");
    }

    @Override
    public void query(String sql) {
        System.out.println("Querying data from MySQL Database: " + sql);
    }
}

public class OracleDatabaseAdapter implements Database {
    private OracleDatabase oracleDatabase;

    public OracleDatabaseAdapter(OracleDatabase oracleDatabase) {
        this.oracleDatabase = oracleDatabase;
    }

    @Override
    public void connect() {
        oracleDatabase.open();
    }

    @Override
    public void query(String sql) {
        oracleDatabase.execute(sql);
    }
}

class OracleDatabase {
    void open() {
        System.out.println("Opening Oracle Database");
    }

    void execute(String sql) {
        System.out.println("Executing SQL on Oracle Database: " + sql);
    }
}

5.8 日誌框架介面卡

許多Java應用程式使用日誌框架來記錄應用程式的執行情況。有許多不同的日誌框架,比如Log4j、SLF4J等,它們有著不同的API。透過使用介面卡模式,我們可以定義一個統一的日誌介面,然後為每個日誌框架實現一個介面卡,從而讓應用程式可以在不同的日誌框架之間無縫切換。

public interface Logger {
    void log(String message);
}

public class Log4jAdapter implements Logger {
    private org.apache.log4j.Logger logger;

    public Log4jAdapter(org.apache.log4j.Logger logger) {
        this.logger = logger;
    }

    @Override
    public void log(String message) {
        logger.info(message);
    }
}

public class SLF4JAdapter implements Logger {
    private org.slf4j.Logger logger;

    public SLF4JAdapter(org.slf4j.Logger logger) {
        this.logger = logger;
    }

    @Override
    public void log(String message) {
        logger.info(message);
    }
}

這樣,我們就可以在應用程式中自由切換使用哪個日誌框架,而不需要修改大量的程式碼。

5.9 第三方庫介面卡

在實際開發中,我們經常會用到第三方庫。但是,不同的庫可能提供了不同的API,直接使用會導致程式碼的耦合度增加。透過介面卡模式,我們可以為每個庫提供一個介面卡,使它們都符合同一個介面,這樣在主程式中就可以無縫切換,降低了程式碼的耦合度。

06.介面卡總結一下

6.1 介面卡使用環境

在以下情況下可以使用介面卡模式:

  1. 系統需要使用現有的類,而這些類的介面不符合系統的需要。
  2. 想要建立一個可以重複使用的類,用於與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起工作。

6.2 介面卡模式擴充

認介面卡模式(Default Adapter Pattern)或預設介面卡模式

當不需要全部實現介面提供的方法時,可先設計一個抽象類實現介面,併為該介面中每個方法提供一個預設實現(空方法),那麼該抽象類的子類可有選擇地覆蓋父類的某些方法來實現需求,它適用於一個介面不想使用其所有的方法的情況。因此也稱為單介面介面卡模式。

一般來說,介面卡模式可以看作一種“補償模式”,用來補救設計上的缺陷

應用這種模式算是“無奈之舉”,如果在設計初期,我們就能協調規避介面不相容的問題,那這種模式就沒有應用的機會了。

6.3 跟其他模式對比

代理、橋接、裝飾器、介面卡,這 4 種模式是比較常用的結構型設計模式。它們的程式碼結構非常相似。

儘管程式碼結構相似,但這4種設計模式的用意完全不同,也就是說要解決的問題、應用場景不同,這也是它們的主要區別。這裡我就簡單說一下它們之間的區別。

  1. 代理模式:代理模式在不改變原始類介面的條件下,為原始類定義一個代理類,主要目的是控制訪問,而非加強功能,這是它跟裝飾器模式最大的不同。
  2. 橋接模式:橋接模式的目的是將介面部分和實現部分分離,從而讓它們可以較為容易、也相對獨立地加以改變。
  3. 裝飾器模式:裝飾者模式在不改變原始類介面的情況下,對原始類功能進行增強,並且支援多個裝飾器的巢狀使用。
  4. 介面卡模式:介面卡模式是一種事後的補救策略。介面卡提供跟原始類不同的介面,而代理模式、裝飾器模式提供的都是跟原始類相同的介面。

07.動態代理總結

7.1 總結一下筆記

01.介面卡模式基礎

什麼叫做介面卡模式?用來做適配,將不相容的介面轉化成相容的介面,例子就是 USB 轉接頭充當介面卡,把兩種不相容的介面,透過轉接變得可以一起工作。

簡單來說,用於事後補救措施,專案程式碼後期,想讓不想關的類,變成可以一起工作。可以作為類結構型模式,也可以作為物件結構型模式。

介面卡模式思考?不要強行適配,設計前期就要考慮到相容性,以減少對介面卡依賴!

02.介面卡模式實現

介面卡模式兩種實現方式:

  1. 類的介面卡。透過繼承來實現
  2. 物件的介面卡。透過組合來實現

03.介面卡模式分析

類介面卡是一種結構型設計模式,它允許將一個類的介面轉換為另一個客戶端所期望的介面。透過繼承原始類和實現目標介面,類介面卡使得原始類的介面與目標介面相容。

物件介面卡是介面卡模式的一種變體,它透過組合(而不是繼承)原始類和目標介面來實現介面卡功能。在物件介面卡中,介面卡類持有一個原始類的例項,並實現目標介面,透過呼叫原始類的方法來實現目標介面的方法。

04.介面卡應用解析

類介面卡:現有一臺電腦只能讀取SD卡,而要讀取TF卡中的內容的話就需要使用到介面卡模式。建立一個讀卡器,將TF卡中的內容讀取出來。

物件介面卡:有一個MediaPlayer介面,它可以播放mp3格式的檔案。現在,我們想擴充套件這個功能,使其也可以播放其他格式的檔案,比如vlc和mp4。

05.應用場景分析

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

  1. 封裝有缺陷的介面設計。
  2. 設計統一多個類的介面。
  3. 替換依賴的外部系統。
  4. 相容老版本介面。
  5. 適配不同格式的資料。

7.2 更多內容推薦

模組描述備註
GitHub多個YC系列開源專案,包含Android元件庫,以及多個案例GitHub
部落格彙總匯聚Java,Android,C/C++,網路協議,演算法,程式設計總結等YCBlogs
設計模式六大設計原則,23種設計模式,設計模式案例,物件導向思想設計模式
Java進階資料設計和原理,物件導向核心思想,IO,異常,執行緒和併發,JVMJava高階
網路協議網路實際案例,網路原理和分層,Https,網路請求,故障排查網路協議
計算機原理計算機組成結構,框架,儲存器,CPU設計,記憶體設計,指令程式設計原理,異常處理機制,IO操作和原理計算機基礎
學習C程式設計C語言入門級別系統全面的學習教程,學習三到四個綜合案例C程式設計
C++程式設計C++語言入門級別系統全面的教學教程,併發程式設計,核心原理C++程式設計
演算法實踐專欄,陣列,連結串列,棧,佇列,樹,雜湊,遞迴,查詢,排序等Leetcode
Android基礎入門,開源庫解讀,效能最佳化,Framework,方案設計Android

23種設計模式

23種設計模式 & 描述 & 核心作用包括
建立型模式
提供建立物件用例。能夠將軟體模組中物件的建立和物件的使用分離
工廠模式(Factory Pattern)
抽象工廠模式(Abstract Factory Pattern)
單例模式(Singleton Pattern)
建造者模式(Builder Pattern)
原型模式(Prototype Pattern)
結構型模式
關注類和物件的組合。描述如何將類或者物件結合在一起形成更大的結構
介面卡模式(Adapter Pattern)
橋接模式(Bridge Pattern)
過濾器模式(Filter、Criteria Pattern)
組合模式(Composite Pattern)
裝飾器模式(Decorator Pattern)
外觀模式(Facade Pattern)
享元模式(Flyweight Pattern)
代理模式(Proxy Pattern)
行為型模式
特別關注物件之間的通訊。主要解決的就是“類或物件之間的互動”問題
責任鏈模式(Chain of Responsibility Pattern)
命令模式(Command Pattern)
直譯器模式(Interpreter Pattern)
迭代器模式(Iterator Pattern)
中介者模式(Mediator Pattern)
備忘錄模式(Memento Pattern)
觀察者模式(Observer Pattern)
狀態模式(State Pattern)
空物件模式(Null Object Pattern)
策略模式(Strategy Pattern)
模板模式(Template Pattern)
訪問者模式(Visitor Pattern)

7.3 更多內容

  • GitHub:https://github.com/yangchong211
  • 我的程式設計網站:https://yccoding.com
  • 部落格彙總:https://github.com/yangchong211/YCBlogs
  • 設計模式專欄:https://github.com/yangchong211/YCDesignBlog
  • Java高階進階專欄:https://github.com/yangchong211/YCJavaBlog
  • 網路協議專欄:https://github.com/yangchong211/YCNetwork
  • 計算機基礎原理專欄:https://github.com/yangchong211/YCComputerBlog

相關文章