設計模式(十)介面卡模式
一、問題引入
說起介面卡其實在我們的生活中是非常常見的,比如:如果你到日本出差,你會發現日本的插座電壓都是110V的,而我們的手機充電器和筆記本充電器都是220V,所以你到了日本之後就沒辦法充電了,這時候我們通常會怎麼辦呢,當然是使用一個升壓的變壓器將電壓升高到220V,這樣我們的手機通過一個變壓器(介面卡)就能使用原本不能使用的插座了。
又比如說,有的國家的插座都是三孔的,而我們的手機大部分都是兩孔的,這是你也沒辦法直接把充電器插到插座上,這時我們可以使用一個介面卡,介面卡本身是三孔的,它可以直接插到三孔的插頭上,介面卡本身可以提供一個兩孔的插座,然後我們的手機充電器就可以插到介面卡上了,這樣我們原本只能插到兩孔上的插頭就能用三孔的插座了。
在我們的面向物件裡也存在這個問題,假設一個軟體系統,你希望它能和一個新的廠商類庫搭配使用,但是這個新廠商所設計出來的介面,不同於舊廠商的介面,就像下圖這樣:
你不想改變現有的程式碼,解決這個問題(而且你也不能改變廠商的程式碼)。所以該怎麼做?這個嘛,你可以寫一個類,將新廠商的介面轉化成你所希望的介面。
這個介面卡工作起來就如同一箇中間人,它將客戶所發出的請求轉換成廠商類能理解的請求。
這樣的話,就能在不改變現有程式碼的情況下使用原本不匹配的類庫了。
二、介面卡模式的相關概念
經過上邊的三個例子,我們可以總結出介面卡模式的使用過程:
1、客戶通過目標介面呼叫介面卡的方法對介面卡發出請求。
2、介面卡使用被適配者介面把請求轉化成被適配者的一個或多個呼叫介面。
3、客戶接收到呼叫的結果,但並未察覺這一切是適配在起轉化作用。
所以介面卡模式的正式定義就是:
介面卡模式將一個類的介面,轉化成客戶期望的另一個介面。介面卡讓原本介面不相容的類可以合作無間。
三、物件介面卡
介面卡其實是分為物件介面卡和類介面卡兩種,兩種的工作原理不太一樣。物件介面卡是使用組合的方法,在Adapter中會保留一個原物件(Adaptee)的引用,介面卡的實現就是講Target中的方法委派給Adaptee物件來做,用Adaptee中的方法實現Target中的方法。
這種型別的好處就是,Adpater只需要實現Target中的方法就好啦。
現在我們通過一個用火雞冒充鴨子的例子來看看如何使用介面卡模式。
package com.designpattern.adapter.object;
public abstract class Duck {
/**
* 嘎嘎叫
*/
public abstract void quack();
public abstract void fly();
}
package com.designpattern.adapter.object;
public abstract class Turkey {
/**
* 火雞叫
*/
public abstract void gobble();
public abstract void fly();
}
package com.designpattern.adapter.object;
public class WildTurkey extends Turkey {
public void gobble() {
System.out.println("Gobble gobble");
}
public void fly() {
System.out.println("I'm flying a short distance");
}
}
package com.designpattern.adapter.object;
/**
* 用火雞冒充鴨子
* @author 98583
*
*/
public class TurkeyAdapter extends Duck {
/**
* 保留火雞的引用
*/
Turkey turkey;
public TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
/**
* 利用火雞的叫聲來實現鴨子的叫聲
*/
public void quack() {
turkey.gobble();
}
/**
* 利用火雞的飛的方法來實現鴨子的飛的方法
*/
public void fly() {
for (int i = 0; i < 5; i++) {
turkey.fly();
}
}
}
package com.designpattern.adapter.object;
/**
* 用火雞冒充鴨子
* @author 98583
*
*/
public class Client {
public static void main(String[] args) {
WildTurkey turkey = new WildTurkey();
Duck turkeyAdapter = new TurkeyAdapter(turkey);
System.out.println("The Turkey says...");
turkey.gobble();
turkey.fly();
System.out.println("\nThe TurkeyAdapter says...");
testDuck(turkeyAdapter);
}
static void testDuck(Duck duck) {
duck.quack();
duck.fly();
}
}
鴨子和火雞有相似之處,他們都會飛,雖然飛的不遠,他們不太一樣的地方就是叫聲不太一樣,現在我們有一個火雞的類,有鴨子的抽象類也就是介面。我們的介面卡繼承自鴨子類並且保留了火雞的引用,重寫鴨子的飛和叫的方法,但是是委託給火雞的方法來實現的。在客戶端中,我們給介面卡傳遞一個火雞的物件,就可以把它當做鴨子來使用了。
四、類介面卡
與物件介面卡不同的是,類介面卡是通過類的繼承來實現的。Adpater直接繼承了Target和Adaptee中的所有方法,並進行改寫,從而實現了Target中的方法。
這種方式的缺點就是必須實現Target和Adaptee中的方法,由於Java不支援多繼承,所以通常將Target設計成介面,Adapter繼承自Adaptee然後實現Target介面。
我們使用類介面卡的方式來實現一下上邊的用火雞來冒充鴨子。
package com.designpattern.adapter.classmethod;
/**
* 由於Java不支援多繼承,所以通常將Target宣告為介面
* @author 98583
*
*/
public interface Duck {
/**
* 嘎嘎叫
*/
public void quack();
public void duckFly();
}
package com.designpattern.adapter.classmethod;
/**
* 目前已有的火雞類的抽象類
* @author 98583
*
*/
public abstract class Turkey {
/**
* 火雞叫
*/
public abstract void gobble();
public abstract void turkeyFly();
}
package com.designpattern.adapter.classmethod;
/**
* 用火雞冒充鴨子,不再保留火雞類的引用,需要實現鴨子類和火雞類的方法
* @author 98583
*
*/
public class TurkeyAdapter extends Turkey implements Duck {
/**
* 利用火雞的叫聲來實現鴨子的叫聲
*/
public void quack() {
gobble();
}
/**
* 利用火雞的飛的方法來實現鴨子的飛的方法
*/
public void turkeyFly() {
for (int i = 0; i < 5; i++) {
System.out.println("I'm flying a short distance");
}
}
/**
* 使用火雞類的方法來實現鴨子類的方法
*/
public void duckFly() {
turkeyFly();
}
/**
* 火雞的叫聲
*/
public void gobble() {
System.out.println("Gobble gobble");
}
}
package com.designpattern.adapter.classmethod;
/**
* 用火雞冒充鴨子
* @author 98583
*
*/
public class Client {
public static void main(String[] args) {
Duck turkeyAdapter = new TurkeyAdapter();
System.out.println("\nThe TurkeyAdapter says...");
testDuck(turkeyAdapter);
}
static void testDuck(Duck duck) {
duck.quack();
duck.duckFly();
}
}
其實兩種方法的效果是一樣的,只是用的方法不一樣。Java不支援多繼承,所以將Duck宣告為介面,Adapter繼承自火雞類並且實現了Duck的方法,但是實現Duck的方法不再是委派給火雞類的物件,而是直接呼叫火雞類的方法,因為在Adapter中實現了火雞類的方法,所以可以直接呼叫。
五、預設介面卡
魯達剃度的故事就很好的說明了預設介面卡的作用。一般的和尚都是吃齋,唸經,打坐,撞鐘和習武,但是魯達只是喝酒喝習武,所以‘魯達不能剃度(不能當做和尚使用),要想讓魯達可以當做和尚使用就要讓他實現和尚的所有方法,但是這樣做時候魯達就不是魯達了。我們可以找一箇中間者,比如魯達是天星的一位,我們可以讓天星實現和尚所有的方法,再讓魯達繼承自天星。程式碼如下:
這是定義的和尚介面,和尚都應該做以下的事。
package com.designpattern.adapter.defaultmethod;
public interface Monk {
public void chizha();
public void nianjing();
public void dazuo();
public void zhuangzhong();
public void xiwu();
}
這是天星類,為每個方法提供一個空實現,其他繼承自該類的子類可以重寫父類的方法。
package com.designpattern.adapter.defaultmethod;
public abstract class Star implements Monk{
public void chizha(){}
public void nianjing(){}
public void dazuo(){}
public void zhuangzhong(){}
public void xiwu(){}
}
魯達繼承自天星,並且添加了喝酒的方法。
package com.designpattern.adapter.defaultmethod;
public class Luda extends Star{
public void xiwu(){
System.out.println("魯達習武");
}
public void hejiu(){
System.out.println("魯達喝酒");
}
}
我們看到通過天星類(預設介面卡),魯達不需要再實現自己不需要的方法了。
六、優缺點
優點:
- 將目標類和適配者類解耦,通過引入一個介面卡類來重用現有的適配者類,而無須修改原有程式碼。
- 增加了類的透明性和複用性,將具體的實現封裝在適配者類中,對於客戶端類來說是透明的,而且提高了適配者的複用性。
- 靈活性和擴充套件性都非常好,通過使用配置檔案,可以很方便地更換介面卡,也可以在不修改原有程式碼的基礎上增加新的介面卡類,完全符合“開閉原則”。
類介面卡模式還具有如下優點:
由於介面卡類是適配者類的子類,因此可以在介面卡類中置換一些適配者的方法,使得介面卡的靈活性更強。
物件介面卡模式還具有如下優點:
一個物件介面卡可以把多個不同的適配者適配到同一個目標,也就是說,同一個介面卡可以把適配者類和它的子類都適配到目標介面。
缺點:
類介面卡模式的缺點如下:
對於Java、C#等不支援多重繼承的語言,一次最多隻能適配一個適配者類,而且目標抽象類只能為抽象類,不能為具體類,其使用有一定的侷限性,不能將一個適配者類和它的子類都適配到目標介面。
物件介面卡模式的缺點如下:
與類介面卡模式相比,要想置換適配者類的方法就不容易。如果一定要置換掉適配者類的一個或多個方法,就只好先做一個適配者類的子類,將適配者類的方法置換掉,然後再把適配者類的子類當做真正的適配者進行適配,實現過程較為複雜。
七、適用環境
1、系統需要使用現有的類,而這些類的介面不符合系統的需要。
2、想要建立一個可以重複使用的類,用於與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起工作。