我們生活中很熟悉的轉換插頭,因為全球有幾種不同的插座介面標準,國內使用的插頭不一定適用於國外一些國家的插座,這時候,就需要用到轉換插頭了。我手中的這個就是德標轉國標的。
你看,這不就類似我們設計模式中的介面卡模式嘛?
將一個類的介面轉換成客戶希望的另外一個介面,使得原來由於介面不相容而不能一起工作的那些類可以一起工作。
文字描述不容易理解沒關係,通過下圖,應該會有一個更直觀的認識。
需求總是會不斷地變化,現在我們需要從舊廠商類切換到新廠商類,由於現有的系統與新廠商類彼此不相容,修改某一個類以相容另外一個類,都不是件輕鬆的事。這時候,就需要介面卡模式來“救場”了。你看,只需要增加一個介面卡的程式碼,就可以了,很輕鬆。
有了直觀的認識之後,我們就來好好地認識一下介面卡模式。
-
Target目標角色:該角色定義把其他類轉換為何種介面,也就是我們的期望介面。
-
Adaptee源角色:你想把誰轉換成目標角色,這個“誰”就是源角色,它是已經存在的,執行良好的類或物件,經過介面卡角色的包裝,它會成為一個嶄新的角色。
-
Adapter介面卡角色:介面卡模式的核心角色,其他兩個角色都是已經存在的角色,而介面卡角色是需要新建立的,它的職責非常簡單:就是把源角色轉換為目標角色。
介面卡模式有兩種實現方式:類介面卡和物件介面卡。其中,類介面卡使用繼承關係來實現,物件介面卡使用組合關係來實現。
類介面卡
類介面卡的重點在於類,是通過構造一個繼承Adaptee類來實現介面卡的功能。
Adaptee:小明進入NBA了,但是他只會用中文溝通,
1 // 已存在的,但是不符合我們既有的標準介面的類 2 public class XiaoMing { 3 private int Num; 4 private String name; 5 6 // 只會用中文溝通 7 public void Chinese() { 8 System.out.println("用中文溝通"); 9 } 10 }
Target:但是球隊教練是法國人,只會法語和英語,不用用中文。
1 // 目標介面,或稱為標準介面 2 public interface Coach { 3 void English(); 4 void French(); 5 }
Adapter:球隊給小明安排了一位翻譯。
1 // 介面卡類:繼承了被適配類,同時實現了標準介面 2 public class Interpreter extends XiaoMing implements Coach { 3 4 @Override 5 public void English() { 6 // 和球員小明用中文溝通 7 super.Chinese(); 8 System.out.println("和教練用英語溝通"); 9 } 10 11 @Override 12 public void French() { 13 // 和球員小明用中文溝通 14 super.Chinese(); 15 System.out.println("和教練用法語溝通"); 16 } 17 }
客戶端
1 public class Demo { 2 public static void main(String[] args) { 3 Coach interpreter = new Interpreter(); 4 5 // 翻譯是雙方溝通的橋樑 6 interpreter.Chinese(); 7 interpreter.English(); 8 interpreter.French(); 9 } 10 }
但是,由於Java中不支援多繼承,所以這個介面卡(翻譯)只能服務於所繼承的被適配者(小明)。如果球隊裡有很多不會講英語和法語的球員,每個球員都安排一位翻譯的話,那成本就會很大,那有什麼辦法解決呢?
這個物件介面卡就派上用場了。
物件適配
物件介面卡的重點在於物件,是通過在直接組合Adaptee類來實現的,而不需要繼承被適配的類。而是通過在介面卡的建構函式中將需要被適配的類傳遞進來,從而進行適配。
Adapter程式碼如下,Target和Adaptee的程式碼同上。
1 public class Interpreter implements Coach { 2 // 與類介面卡不同的是,物件介面卡可以適配多個源到目標 3 private Xiaoming xiaoming; 4 // 比如,還可以組合其他Adaptee 5 // private Tony tony; // 講俄語 6 7 // 在建構函式中將Adaptee類Xiaoming傳遞進來 8 public Interpreter(Xiaoming xiaoming) { 9 this.xiaoming = xiaoming; 10 } 11 12 // 實現介面中的方法 13 @Override 14 public void English() { 15 // 和球員小明用中文溝通 16 this.xiaoming.Chinese(); 17 // 和教練用英語溝通 18 System.out.println("用英語溝通"); 19 } 20 21 @Override 22 public void French() { 23 // 和球員小明用中文溝通 24 this.xiaoming.Chinese(); 25 // 和教練用法語溝通 26 System.out.println("用法語溝通"); 27 } 28 }
客戶端
1 public class Demo { 2 public static void main(String[] args) { 3 Coach interpreter = new Interpreter(new XiaoMing()); 4 5 interpreter.English(); 6 interpreter.French(); 7 } 8 }
兩種方式應該如何選擇?
判斷的標準主要有兩個,一個是Adaptee介面的個數,另一個是Adaptee和Target的契合程度。
-
如果Adaptee介面並不多,那兩種實現方式是都可以;
-
如果Adaptee介面很多,而且Adaptee和Target介面定義大部分都相同,那我們推薦使用類介面卡,因為Adapter複用父類Adaptee的介面,比起物件介面卡的實現方式,Adapter的程式碼量要少一些。
-
如果Adaptee介面很多,而且Adaptee和Target介面定義大部分都不相同,那我們推薦使用物件介面卡,因為組合結構相對於繼承更加靈活。
介面卡模式的優點
-
介面卡模式可以讓兩個沒有關係的類在一起執行,只要介面卡這個角色能夠搞定他們就可以。
-
增加了類的透明性
想想看,我們訪問的Target目標角色,但是具體的實現都委託給了源角色,而這些對高層次模組是透明的,也是它不需要關心的。
-
提高了類的複用度
源角色在原有的系統中還是可以正常使用的,而在目標角色中也可以充當新的演員。
-
靈活性非常好
如果突然不想要介面卡,沒有問題,刪除掉這個介面卡就可以了,其他的程式碼都不用修改,基本上就類似一個靈活的構件,想用就用,不用就解除安裝。
介面卡模式的缺點
-
對於類介面卡來說,由於Java、C#等不支援多重繼承的語言,一次最多隻能適配一個適配者類,而且目標抽象類只能為介面,不能為類,其使用有一定的侷限性,不能將一個適配者類和它的子類同時適配到目標介面。
-
對於物件介面卡來說,與類介面卡模式相比,要想置換適配者類的方法就不容易。
-
過多地使用介面卡,會讓系統非常凌亂,不容易整體進行把握。
比如,明明看到呼叫的是A介面,其實內部被適配成了B介面來實現,一個系統如果出現太多這種情況,無異於一場災難。
介面卡模式的應用場景
一般來說,介面卡模式可以看作是一種“補償模式”,用來補救設計上的缺陷。應用這種模式算是“無奈之舉”。如果在設計初期,就能協調規避介面不相容的問題,那麼,介面卡模式就沒有應用的機會了。
-
封裝有缺陷的介面設計
如果我們依賴的外部系統在介面設計方面有缺陷(比如包含大量靜態方法),引入之後會影響到我們自身程式碼的可測試性。為了隔離設計上的缺陷,我們希望對外部系統提供的介面進行二次封裝,抽象出更好的介面設計,這個時候就可以使用介面卡模式了。
-
統一多個類的介面設計
某個功能的實現依賴多個外部系統(或者說類)。通過介面卡模式,將它們的介面適配為統一的介面定義,然後我們就可以使用多型的特性來複用程式碼邏輯。
-
替換依賴的外部系統
當我們把專案中依賴的一個外部系統替換為另一個外部系統的時候,利用介面卡模式,可以減少對程式碼的改動。
-
相容老版本介面
在做版本升級的時候,對於一些要廢棄的介面,我們不直接將其刪除,而是暫時保留,並且標註為deprecated,並將內部實現邏輯委託為新的介面實現。這樣做的好處是,讓使用它的專案有個過渡期,而不是強制進行程式碼修改。這也可以粗略地看做介面卡模式的一個應用場景。
-
適配不同格式的資料
介面卡模式主要用於介面的適配,實際上,它還可以用在不同格式的資料之間的適配。比如,把從不同徵信系統拉取的不同格式的徵信資料,統一為相同的格式,以方便儲存和使用。再比如,Java中的Arrays.asList()也可以看作一種資料介面卡,將資料型別的資料轉化為集合容器型別。
總結
介面卡模式主要應用於希望複用一些現存的類,但是介面又與複用環境要求不一致的情況。
參考
《設計模式之禪》
極客時間專欄《設計模式之美》
《Head First 設計模式》
https://www.jb51.net/article/100146.htm