各位朋友好,本章節我們繼續講第五個設計模式。 在生活中,我們都知道手機記憶體卡是無法直接接電腦的,因為記憶體卡的卡槽比較小,而電腦只有USB插孔,此時我們需要用到讀卡器。這個讀卡器就相當於是適配器。這是生活上的適配器,那麼在OO對象中,適配器就是將一個介面轉換成另一個介面,使得客戶可以使用。 適配器模式 ...
各位朋友好,本章節我們繼續講第五個設計模式。
在生活中,我們都知道手機記憶體卡是無法直接接電腦的,因為記憶體卡的卡槽比較小,而電腦只有USB插孔,此時我們需要用到讀卡器。這個讀卡器就相當於是適配器。這是生活上的適配器,那麼在OO對象中,適配器就是將一個介面轉換成另一個介面,使得客戶可以使用。
適配器模式從實現方式上分為兩種,類適配器和對象適配器,這兩種的區別在於實現方式上的不同,一種採用繼承,一種採用組合的方式。
下麵我們來看一個例子,下麵有兩個介面,一個是鹿(Deer),一個是狼(wolf),
public interface Deer {
public void run();
public void eatGrass();
}
interface Wolf{
public void run();
public void eatMeat();
}
我們讓梅花鹿(SikaDeer)和雪狼(SnowWolf)分別實現這兩個介面
class SikaDeer implements Deer{
@Override
public void run() {
System.out.println("我在跑");
}
@Override
public void eatGrass() {
System.out.println("我在吃草");
}
}
class SnowWolf implements Wolf{
@Override
public void run() {
System.out.println("我在跑");
}
@Override
public void eatMeat() {
System.out.println("我在吃肉");
}
}
假設現在狼要吃鹿,但是他要偽裝成鹿然後混進去,那麼現在因為介面不同,無法偽裝,所以現在我們幫它寫個適配器:
class SnowAdapter implements Deer{//首先我們需要實現想轉換成的介面,也就是鹿要看到的介面
private SnowWolf snowWolf;
public SnowAdapter(SnowWolf snowWolf) {
this.snowWolf = snowWolf;//接著取得我們要適配的對象的引用
}
@Override
public void run() {
snowWolf.run();//接著實現介面的方法
}
@Override
public void eatGrass() {
snowWolf.eatMeat();
}
}
接下來我們寫個測試類:
public class TestAdapter {
public static void main(String[] args) {
Deer sikaDeer = new SikaDeer();//先來個梅花鹿
SnowWolf snowWolf = new SnowWolf();//再來個雪狼
SnowAdapter snowAdapter = new SnowAdapter(snowWolf);//接下來是偽裝後的雪狼
System.out.println("snowWolf:");
snowWolf.run();
snowWolf.eatMeat();
System.out.println("sikaDeer says:");
testDeer(sikaDeer);
System.out.println("snowAdapter says:");
testDeer(snowAdapter);
}
public static void testDeer(Deer deer){
deer.run();
deer.eatGrass();
}
}
結果展示:
在這裡,我們來分析一下這個過程,鹿是我們的目標介面,梅花鹿是依據鹿實現的,而適配器實現了目標介面,並持有被適配者(雪狼)的實例。
適配器模式講一個類的介面,轉換成客戶期望的另一個。適配器讓原本介面不相容的類可以合作無間。
下麵我們看一下類圖:
這種實現方式是對象適配器,接下來LZ說一下類適配器
從類圖中可以看出,客戶想要的是sampleOperation2()方法,但是Adaptee並沒有,為了使客戶能夠使用Adaptee類的sampleOperation2()方法,我們需要提供一個構造器Adapter,把Adaptee的API與Target類的API銜接起來。Adapter與Adaptee是繼承關係。
這裡所涉及的角色有:
● 目標(Target)角色:這就是所期待得到的介面。註意:由於這裡討論的是類適配器模式,因此目標不可以是類。
● 源(Adapee)角色:現在需要適配的介面。
● 適配器(Adaper)角色:適配器類是本模式的核心。適配器把源介面轉換成目標介面。顯然,這一角色不可以是介面,而必須是具體類。
我們用代碼簡單的模仿一下類圖的關係,首先是目標角色
public interface Target {
/**
* 這是源類Adaptee也有的方法
*/
public void sampleOperation1();
/**
* 這是源類Adapteee沒有的方法
*/
public void sampleOperation2();
}
接著是源,
public class Adaptee {
public void sampleOperation1(){}
}
接下來我們寫適配器,我們讓適配器直接繼承源,並實現目標介面,然後補充sampleOperation2()方法
public class Adapter extends Adaptee implements Target {
@Override
public void sampleOperation2() {
//寫相關的代碼
}
}
類適配器所用的是繼承,但是因為JAVA單繼承的原因,一個JAVA類只能有一個父類,所以當我們要適配的對象是兩個類的時候,我們就無可奈何了。
接下來我們看一下類適配器與對象適配器之間的不同:
①類適配器使用對象繼承的方式,是靜態的定義方式;而對象適配器使用對象組合的方式,是動態組合的方式。
②對於類適配器,由於適配器直接繼承了Adaptee,使得適配器不能和Adaptee的子類一起工作,因為繼承是靜態的關係,當適配器繼承了Adaptee後,就不可能再去處理 Adaptee的子類了。而對於對象適配器,一個適配器可以把多種不同的源適配到同一個目標。換言之,同一個適配器可以把源類和它的子類都適配到目標介面。因為對象適配器採用的是對象組合的關係,只要對象類型正確,是不是子類都無所謂。
③ 對於類適配器,適配器可以重定義Adaptee的部分行為,相當於子類覆蓋父類的部分實現方法。而對於對象適配器,要重定義Adaptee的行為比較困難,這種情況下,需要定義Adaptee的子類來實現重定義,然後讓適配器組合子類。雖然重定義Adaptee的行為比較困難,但是想要增加一些新的行為則方便的很,而且新增加的行為可同時適用於所有的源。
④對於類適配器,僅僅引入了一個對象,並不需要額外的引用來間接得到Adaptee。而 對於對象適配器,需要額外的引用來間接得到Adaptee。
那麼到底是多用組合還是多用繼承,相信大家經過了前面四章的學習,已經非常明瞭了。LZ建議儘量使用對象適配器的實現方式,多用合成/聚合、少用繼承。不過說到底,具體問題還要進行具體分析才對,根據需要來選用實現方式,最適合的才是最好的。
適配器模式的優點
- 更好的復用性
系統需要使用現有的類,而此類的介面不符合系統的需要。那麼通過適配器模式就可以讓這些功能得到更好的復用。
- 更好的擴展性
在實現適配器功能的時候,可以調用自己開發的功能,從而自然地擴展系統的功能。
適配器模式的缺點
過多的使用適配器,會讓系統非常零亂,不易整體進行把握。比如,明明看到調用的是A介面,其實內部被適配成了B介面的實現,一個系統如果太多出現這種情況,無異於一場災難。因此如果不是很有必要,可以不使用適配器,而是直接對系統進行重構
上面LZ說過適配器從實現方式上分為兩種,類適配器和對象適配器,其實從使用目的上來說,也可以分為兩種,特殊適配器和預設適配器,這兩種的區別在於使用目的上的不同,一種為了復用原有的代碼並適配當前的介面,一種為了提供預設的實現,避免子類需要實現不該實現的方法。
而我們以上兩種方式都是為了復用現有的代碼而採用的適配器模式,屬於特殊適配器,可稱為定製適配器,還有另外一種稱為預設適配器。
下麵LZ舉一個簡單的例子:
interface Teacher{
void speak();
void listen();
void teach();
}
這是一個老師的介面,張老師要實現它,但是幾年前,張老師還只是一個學生,並不會teach(),所以我們需要寫一個預設適配器
abstract class Adapter implements Teacher{
@Override
public void speak() {}
@Override
public void listen() {}
@Override
public void teach() {}
}
然後讓張老師繼承它
class ZhangTeacher extends Adapter{
@Override
public void speak() {
System.out.println("speak");
}
@Override
public void listen() {
System.out.println("listen");
}
}
雖然這隻是一個例子,但是在很多情況下,我們必須讓一個具體類實現某一個介面,但是這個類又用不到介面所規定的所有的方法。通常的處理方法是,這個具體類要實現所有的方法,那些有用的方法要有實現,那些沒有用的方法也要有空的實現。
這些空的方法是一種浪費,有時也是一種混亂。除非看過這些空方法的代碼,否則程式員可能會以為這些方法不是空的。即便他知道其中有一些方法是空的,也不一定知道哪些方法是空的,哪些方法不是空的,除非看過這些方法的源代碼或是文檔。
而預設適配模式可以很好的處理這一情況。可以設計一個抽象的適配器類實現介面,此抽象類要給介面所要求的每一種方法都提供一個空的方法,使它的具體子類免於被迫實現空的方法。
在任何時候,如果我們不准備實現一個介面的所有方法時,就可以使用“預設適配模式”製造一個抽象類,給出所有方法的具體實現。這樣,從這個抽象類再繼承下去的子類就不必實現所有的方法了。
這一章講完,適配器模式相信各位也有了一定的瞭解,此模式其實就是一種補救措施,在開發的時候,儘量不要用到這個模式。