設計模式之適配器、外觀模式

DesignPattern之Adapter和Facade

迪米特原則(”Least Knowledge”原則)

Adapter 適配器模式(主要目的:轉化接口!)

1.定義


將一個類的接口,轉換成客戶期望的另一個接口。適配器讓原來接口不兼容的類可以合作無間。

個人理解:適配器模式,就好比轉換插。我的電腦的充電器是英標三腳的插頭,無法插進歐標的二腳插座,要解決這個問題,只需要一個歐標轉換插,電腦充電器插入這個轉換插,然後這個轉換插再插進歐標的插座就可以解決。而且不僅是電腦充電器,凡是英標三腳的插頭的電器都能使用這個轉換插(只要實現了接口,被適配者的任何子類,都可以搭配)。

  • 適配器模式可以通過創建適配器進行接口轉換,讓原來不兼容的接口變成兼容。這可以讓客戶從實現的接口解耦。如果在一段時間後想要改變接口,適配器可以將改變的部分封裝起來,客戶就不必爲了應對不同的接口而每次跟着修改。

  • 適配器模式充滿着良好的OO設計原則:使用對象組合,以修改的接口包裝被適配者(適配器這個類中持有被適配者的引用)–> 額外優點:被適配者的任何子類都可以搭配適配器使用。

  • 因爲客戶是和接口綁定起來(面向接口編程),而不是和實現綁定起來,所以可以使用數個適配器,每一個負責轉換不同的後臺類;或者也可以加上新的實現,==只要實現了目標接口即可!==

2.例子:


使用火雞搭配適配器來冒充鴨子

鴨子接口:

public interface Duck {

    public void quack();
    public void fly();

}

綠頭鴨實現了鴨子這個接口:

public class MallarDuck implements Duck {

    @Override
    public void quack() {
        System.out.println("Quack!");
    }

    @Override
    public void fly() {
        System.out.println("MallarDuck flying!");
    }

}

火雞接口:


public interface Turkey {

    public void gobble();
    public void fly();

}

火雞的實現類:

public class WildTurkey implements Turkey {

    @Override
    public void gobble() {
        System.out.println("Gobble gobble!");
    }

    @Override
    public void fly() {
        System.out.println("I am flying a short distance!");
    }

}

現在缺少鴨子對象,想用一些火雞對象來冒充鴨子

–> 使用火雞-->鴨子適配器!!!!

// 首先這個適配器類需要實現想轉換成的類型的接口,也就是客戶所期望看到的接口
// 就好像前面所說的轉換插,如果這個轉換插不實現期望的接口(不是二腳歐標插口),
// 那麼這個轉換插並沒有什麼卵用!
//
public class TurkeyAdapter implements Duck {

    // 需要取得適配的對象(被適配者)的引用
    private Turkey turkey;

    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }

    //實現了期望的接口就必須實現接口的方法,這裏相當於委託給被適配者來實現
    @Override
    public void quack() {
        turkey.gobble();
    }

    @Override
    public void fly() {
        for (int i = 0; i < 5; i++) {
            turkey.fly();
        }
    }

}

客戶類:

public class AdapterTest {

    public static void main(String[] args) {
        MallarDuck mDuck = new MallarDuck();

        WildTurkey turkey = new WildTurkey();

        // 將火雞包裝進一個火雞適配器中,使他看起來像是一隻鴨子
        // 注意這裏的類型是鴨子接口,就是說在客戶看來這是一隻鴨子
        Duck turkeyAdapter = new TurkeyAdapter(turkey);

        System.out.println("\nThe Turkey says...");
        turkey.gobble();
        turkey.fly();

        System.out.println("\nDuck says...");
        testDuck(mDuck);

        System.out.println("\nThe turkeyAdapter says...");

        // 因爲turkeyAdapter的類型是Duck,所以完全可以作爲參數傳進testDuck方法
        // 但Duck的外表下其實是一種火雞。。。坑爹啊!!!
        testDuck(turkeyAdapter); 
    }

    private static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }

}

3.java中的適配器模式


java早期的集合類型(Vector,Stack, Hashtable等)都有一個名爲elements()的方法,該方法的返回值類型爲Enumeration, Enumeration裏有兩個方法hasMoreElements(), nextElement()

Sun推出更新後的集合類是,開始使用Iterator接口,與Enumeration類似,但還提供了remove()方法。

面對遺留代碼,這些代碼暴露出枚舉類接口,但是我們想在新的代碼中使用迭代器?
–> 使用一個適配器即可。

按照前面鴨子的例子:

Enumeration接口:

public interface Enumeration {
    public void hasMoreElements();
    public void nextElement();
}

Iterator接口:

public interface Iterator {
    public boolean hasNext();
    public Object next();
    public void remove();
}

重頭戲:
適配器!

public class EnumerationAdapter implements Iterator {
    private Enumeration enum;

    public EumerationAdapter(Enumeration enum) {
        this.enum = enum;
    }

    @Override
    public boolean hasNext() {
        return this.enum.hasMoreElements();
    }

    @Override
    public void next() {
        this.enum.nextElement();
    }

    // 我們知道枚舉不支持刪除,因爲枚舉是一個只讀的接口。
    // 適配器無法實現一個有實際功能的remove方法
    // 幸運的時, 迭代器接口的設計者事先料到了會有這樣的需要
    // 所以將remove()方法定義成會拋出 UnsupportedOperationException

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

2. Facade 外觀模式(主要目的:簡化接口!)

定義

提供了一個統一的接口,用來訪問子系統中的一羣接口。外觀模式定義了一個高層接口, 讓子系統更容易使用。

例子

假設家裏有一套家庭影院

觀賞電影(複雜的方式)

  • 打開爆米花機
  • 開始爆米花
  • 將燈光調暗
  • 放下屏幕
  • 打開投影機
  • 將投影機的輸入切換到DVD
  • 投影機設置成寬屏模式
  • 打開功放
  • 將功放的輸入設置爲DVD
  • 將功放設置爲環繞立體聲
  • 打開DVD播放器
  • 開始播放DVD

映射成代碼:

popper.on();
popper.pop();

lights.dim(10);

screen.down();

projector.on();
projector.setInput(dvd);
projector.wideScreenMode();

amp.on();
amp.setDvd(dvd);
amp.setSurroundSound();
amp.setVolume(5);

dvd.on();
dvd.play();

媽蛋,如果軟甲設計成這麼麻煩,哪裏會有客戶會用這個軟件!!!

所以要使用==外觀模式==,通過實現一個體更合理的接口的外觀類,將一個複雜的子系統變得易用。

在這裏,可以把上面的代碼封裝進一個watchMovie()方法中,客戶只需要調用這個方法,就可以完成一系列的準備工作(好比按下遙控器上的一個按鈕,然後系統就自動準備好。)

public class HomeTheaterFacade {
    AMplifier amp;
    Tuner tuner;
    DvdPlayer dvd;
    cdPlayer cd;
    Projector projector;
    TheaterLights lights;
    Screen screen;
    PopcornPopper popper;

    public HomeTheaterFacad(AMplifier amp,
    Tuner tuner,
    DvdPlayer dvd,
    cdPlayer cd,
    Projector projector,
    TheaterLights lights,
    Screen screen,
    PopcornPopper popper) {
        this.amp = amp;
        this.tuner = tuner;
        this.dvd = dvd;
        this.cd = cd;
        this.projector = projector;
        this.screen = screen;
        this.lights = lights;
        this.popper = popper;
    }

    //外觀模式!!!
    public void watchMovie(String movie) {
        popper.on();
        popper.pop();

        lights.dim(10);

        screen.down();

        projector.on();
        projector.setInput(dvd);
        projector.wideScreenMode();

        amp.on();
        amp.setDvd(dvd);
        amp.setSurroundSound();
        amp.setVolume(5);

        dvd.on();
        dvd.play();
    }
}

客戶類:


public class Client {
    public static void main(String[] args) {
        HomeTheaterFacade homeTheater = new HomeTheaterFacade(amp, 
        tuner, dvd, cd, projector, screen, lights, popper);

        // 客戶只需要調用watchMovie方法便可以看電影了,
        // 比沒有使用外觀模式之前方便了無數倍!!!!
        homeTheater.watchMovie("Matrix");
    }
}

注意:外觀模式並沒有“封裝“子系統的類,只是提供了簡化的接口。所以如果客戶覺得有必要,仍然可以直接使用子系統的類。這也是外觀模式的一個優點:==提供簡化的接口的同時,依然將系統外爭的功能暴露出來,以供需要的人使用。==

3.迪米特原則(”Least Knowledge”原則)

這個原則告訴我們要減少對象之間的交互,只留下幾個”密友“,讓朋友圈維持在最小狀態!!!!

這是說:當你在設計一個系統的時候,不管是任何對象,你都要注意它所交互的類有哪些,並注意這些類是如何交互的。

==–>不要讓太多的類耦合在一起(低耦合!!!), 免得修改系統中一部分,會影響到其他部分==

如果許多類之間相互依賴,那麼這個系統就會變成一個易碎的系統,而且極其複雜(想象一下一堆毛線纏繞在一起。。。)

如何做到這個原則?

就任何對象而言,在該對象的方法內,只應該調用屬於以下範圍的方法:

  • 該對象本身的

  • 被當做方法的參數而傳遞進來的對象

  • 此方法所創建或實例化的對象

    前面三條告訴我們:如果某對象調用其他方法的方法,不要調用該對象的方法!!!!

  • 對象的任何組件 (該類的屬性的對象(該類持有對方引用))

調用從另一個調用中返回的對象的方法,會有什麼壞處?

如果這樣做,相當於想另一個對象的子部分發送請求,從而增加了直接認識的對象數目 –> 該類依賴的類的數目增加,系統的穩定性下降!



例子1

不採用這個原則:

public float getTemp() {
    Thermometer thermometer = station.getThermometer();
    return thermometer.getTemperature();
}

這裏的termometer對象時由station的方法返回而來,這樣做的結果是:當前的類與Thermometer這個類產生了關聯,相當於當前類要認識Thermometer和Station兩個類。。。

==採用這個原則==

在Station中添加一個方法,用來向Thermometer取得溫度,這樣就可以從Station這個類返回溫度, 當前類無需與Thermometer產生關聯(降低耦合!)

public float getTemp() {
    return station.getTemperature();
}


外觀模式和最少知識原則

回想一下前面的家庭影院的設計,如果不採用外觀模式,那麼客戶這個類必須要與Tuner,Screen,CdPlayer等各種類打交道,類與類之間的聯繫十分複雜。但是採用外觀模式之後,客戶只需要與HomeTheaterFacade這個類打交道,調用watchMovie方法便可。類與類之間的關聯、複雜度大大降低了!

試着讓子系統也能遵守這個原則:如果子系統太複雜,有太多的朋友牽涉其中,那麼我們可以增加更多的外觀,將此子系統分成好幾個層次。

裝飾者、適配器、外觀三種模式對比

模式 目的
裝飾者 不改變藉口,但加入新功能、新責任
適配器 將一個接口轉換成用戶需要的接口
外觀 使接口更簡單易用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章