一文帶你看懂Java適配器設計模式

1 動機

類似於電源適配器的設計和編碼技巧。一般客戶端通過目標類的接口訪問它所提供的服務。
有時,現有類可以滿足客戶類需要,但所提供接口不一定是客戶類所期望的,可能因爲現有類中方法名與目標類中定義的方法名不一致

這時,現有接口需要轉化爲客戶類期望的接口,保證複用現有類
如果不進行這樣的轉化,客戶類就不能利用現有類所提供的功能,適配器模式可以完成這樣的轉化。

在適配器模式中可以定義一個包裝類,包裝不兼容接口的對象

  • 包裝類指的就是適配器(Adapter)
  • 所包裝的對象就是適配者(Adaptee),即被適配的類

適配器提供客戶類需要的接口
適配器的實現就是把客戶類的請求轉化爲對適配者的相應接口的調用。即當客戶類調用適配器的方法時,在適配器類的內部將調用適配者類的方法,而這個過程對客戶類是透明的,客戶類並不直接訪問適配者類。因此,適配器可以使由於接口不兼容而不能交互的類可以一起工作

2 定義

將一個接口轉換成客戶希望的另一個接口,使接口不兼容的那些類可以一起工作,其別名爲包裝器
既可以作爲類結構型模式,也可以作爲對象結構型模式。

3 結構

  • Target:目標抽象類
  • Adapter:適配器類
  • Adaptee:適配者類
  • Client:客戶類

適配器模式有對象適配器和類適配器兩種實現:

3.1 對象適配器

3.2 類適配器

#4 時序圖

5 代碼分析

實現

  • MediaPlayer 接口
  • 實現 MediaPlayer 接口的實體類 AudioPlayer

默認情況下,AudioPlayer 可以播放 mp3

  • 接口 AdvancedMediaPlayer
  • 實現了 AdvancedMediaPlayer 接口的實體類。該類可以播放 vlc 和 mp4 格式的文件。

我們想要讓 AudioPlayer 播放其他格式的音頻文件。爲了實現這個功能,我們需要創建

  • 一個實現了 MediaPlayer 接口的適配器類 MediaAdapter

使用

  • AdvancedMediaPlayer 對象來播放所需的格式。

AudioPlayer 使用適配器類 MediaAdapter 傳遞所需的音頻類型,不需要知道能播放所需格式音頻的實際類。AdapterPatternDemo,我們的演示類使用 AudioPlayer 類來播放各種格式。

適配器模式的 UML 圖

步驟 1

爲媒體播放器和更高級的媒體播放器創建接口。

MediaPlayer.java

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

AdvancedMediaPlayer.java

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

步驟 2

創建實現了 AdvancedMediaPlayer 接口的實體類

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) {
      //什麼也不做
   }
}

Mp4Player.java

public class Mp4Player implements AdvancedMediaPlayer{

   @Override
   public void playVlc(String fileName) {
      //什麼也不做
   }

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

步驟 3

創建實現了 MediaPlayer 接口的適配器類。

MediaAdapter.java

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);
      }
   }
}

步驟 4

創建實現了 MediaPlayer 接口的實體類。

AudioPlayer.java

public class AudioPlayer implements MediaPlayer {
   MediaAdapter mediaAdapter; 

   @Override
   public void play(String audioType, String fileName) {        

      //播放 mp3 音樂文件的內置支持
      if(audioType.equalsIgnoreCase("mp3")){
         System.out.println("Playing mp3 file. Name: "+ fileName);            
      } 
      //mediaAdapter 提供了播放其他文件格式的支持
      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");
      }
   }   
}

步驟 5

使用 AudioPlayer 來播放不同類型的音頻格式。

AdapterPatternDemo.java

public class AdapterPatternDemo {
   public static void main(String[] args) {
      AudioPlayer audioPlayer = new AudioPlayer();

      audioPlayer.play("mp3", "beyond the horizon.mp3");
      audioPlayer.play("mp4", "alone.mp4");
      audioPlayer.play("vlc", "far far away.vlc");
      audioPlayer.play("avi", "mind me.avi");
   }
}

步驟 6

驗證輸出。

Playing mp3 file. Name: beyond the horizon.mp3
Playing mp4 file. Name: alone.mp4
Playing vlc file. Name: far far away.vlc
Invalid media. avi format not supported

案例

實現 IterableFibnoacci 生成器

重寫這個類,實現 Iterable 接口?

  • 不過你並不是總擁有源代碼的控制權
  • 並且,除非必須這麼做,否則,我們也不願意重寫一個類

因此另一種選擇,創建一個 適配器 (Adapter) 來實現所需的接口

有多種適配器的實現。例如繼承:

for-in 語句中使用 IterableFibonacci,必須在構造函數中提供一個邊界值,這樣 hasNext() 才知道何時返回 false,結束循環。

8 優點

  • 將目標類和適配者類解耦,通過引入一個適配器類來重用現有的適配者類,而無須修改原有代碼。
  • 增加了類的透明性和複用性,將具體的實現封裝在適配者類中,對於客戶端類來說是透明的,而且提高了適配者的複用性。
  • 靈活性和擴展性都非常好,通過使用配置文件,可以很方便地更換適配器,也可以在不修改原有代碼的基礎上增加新的適配器類,完全符合“開閉原則”。

類適配器模式還具有如下優點:
由於適配器類是適配者類的子類,因此可以在適配器類中置換一些適配者的方法,使得適配器的靈活性更強。
對象適配器模式還具有如下優點:
一個對象適配器可以把多個不同的適配者適配到同一個目標,也就是說,同一個適配器可以把適配者類和它的子類都適配到目標接口。

9 缺點

類適配器模式

對於Java、C#等不支持多重繼承的語言,一次最多隻能適配一個適配者類,而且目標抽象類只能爲抽象類,不能爲具體類,其使用有一定的侷限性,不能將一個適配者類和它的子類都適配到目標接口。

對象適配器模式

與類適配器模式相比,要想置換適配者類的方法就不容易。如果一定要置換掉適配者類的一個或多個方法,就只好先做一個適配者類的子類,將適配者類的方法置換掉,然後再把適配者類的子類當做真正的適配者進行適配,實現過程較爲複雜。

10 適用環境

在以下情況下可以使用適配器模式:

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

11 模式應用

Sun公司在1996年公開了Java語言的數據庫連接工具JDBC,JDBC使得Java語言程序能夠與數據庫連接,並使用SQL語言來查詢和操作數據。JDBC給出一個客戶端通用的抽象接口,每一個具體數據庫引擎(如SQL Server、Oracle、MySQL等)的JDBC驅動軟件都是一個介於JDBC接口和數據庫引擎接口之間的適配器軟件。抽象的JDBC接口和各個數據庫引擎API之間都需要相應的適配器軟件,這就是爲各個不同數據庫引擎準備的驅動程序。

12 模式擴展

認適配器模式(Default Adapter Pattern)或缺省適配器模式
當不需要全部實現接口提供的方法時,可先設計一個抽象類實現接口,併爲該接口中每個方法提供一個默認實現(空方法),那麼該抽象類的子類可有選擇地覆蓋父類的某些方法來實現需求,它適用於一個接口不想使用其所有的方法的情況。因此也稱爲單接口適配器模式。

13 總結

  • 結構型模式:描述如何將類或者對象結合在一起形成更大的結構。
    適配器模式用於將一個接口轉換成客戶希望的另一個接口,適配器模式使接口不兼容的那些類可以一起工作,其別名爲包裝器。適配器模式既可以作爲類結構型模式,也可以作爲對象結構型模式。
  • 適配器模式包含四個角色:目標抽象類定義客戶要用的特定領域的接口;適配器類可以調用另一個接口,作爲一個轉換器,對適配者和抽象目標類進行適配,它是適配器模式的核心;適配者類是被適配的角色,它定義了一個已經存在的接口,這個接口需要適配;在客戶類中針對目標抽象類進行編程,調用在目標抽象類中定義的業務方法。
  • 在類適配器模式中,適配器類實現了目標抽象類接口並繼承了適配者類,並在目標抽象類的實現方法中調用所繼承的適配者類的方法;在對象適配器模式中,適配器類繼承了目標抽象類並定義了一個適配者類的對象實例,在所繼承的目標抽象類方法中調用適配者類的相應業務方法。
  • 適配器模式的主要優點是將目標類和適配者類解耦,增加了類的透明性和複用性,同時系統的靈活性和擴展性都非常好,更換適配器或者增加新的適配器都非常方便,符合“開閉原則”;類適配器模式的缺點是適配器類在很多編程語言中不能同時適配多個適配者類,對象適配器模式的缺點是很難置換適配者類的方法。
  • 適配器模式適用情況包括:系統需要使用現有的類,而這些類的接口不符合系統的需要;想要建立一個可以重複使用的類,用於與一些彼此之間沒有太大關聯的一些類一起工作。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章