設計模式(十)適配器模式
一、問題引入
說起適配器其實在我們的生活中是非常常見的,比如:如果你到日本出差,你會發現日本的插座電壓都是110V的,而我們的手機充電器和筆記本充電器都是220V,所以你到了日本之後就沒辦法充電了,這時候我們通常會怎麼辦呢,當然是使用一個升壓的變壓器將電壓升高到220V,這樣我們的手機通過一個變壓器(適配器)就能使用原本不能使用的插座了。
又比如說,有的國家的插座都是三孔的,而我們的手機大部分都是兩孔的,這是你也沒辦法直接把充電器插到插座上,這時我們可以使用一個適配器,適配器本身是三孔的,它可以直接插到三孔的插頭上,適配器本身可以提供一個兩孔的插座,然後我們的手機充電器就可以插到適配器上了,這樣我們原本只能插到兩孔上的插頭就能用三孔的插座了。
在我們的物件導向裡也存在這個問題,假設一個軟體系統,你希望它能和一個新的廠商類庫搭配使用,但是這個新廠商所設計出來的介面,不同於舊廠商的介面,就像這樣:
你不想改變現有的代碼,解決這個問題(而且你也不能改變廠商的代碼)。所以該怎麼做?這個嘛,你可以寫一個類,將新廠商的介面轉化成你所希望的介面。<喎?http://www.bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="這裡寫圖片描述" src="http://www.bkjia.com/uploads/allimg/151205/042H639B-1.png" title="\" />
這個適配器工作起來就如同一個中間人,它將客戶所發出的請求轉換成廠商類能理解的請求。
這樣的話,就能在不改變現有代碼的情況下使用原本不匹配的類庫了。
二、適配器模式的相關概念
經過上邊的三個例子,我們可以總結出適配器模式的使用過程:
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(The 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(The 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、想要建立一個可以重複使用的類,用於與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起工作。