天天看點

headfirst設計模式(8)—擴充卡模式與外觀模式

前言

這一章主要講2個模式,一個是,擴充卡模式(負責将一個類的接口适配成使用者所期待的),另外一個是外觀模式(為子系統提供一個共同的對外接口),看完的第一反應是,為什麼要把它們兩放在同一章,難道它們有什麼不可告人的秘密?

難道是因為他們倆都很簡單嗎?不會不會,畢竟是大名鼎鼎的headfirst,怎麼可能這麼草率,這我是萬萬不相信的!

細想了一下,我和工作的點點滴滴,我發現,一般到項目的後期,好像都比較容易用上這兩個東西...

當然,項目的後期并不是說一個項目自己從頭發開到尾的項目,而是在它生命周期的後半段,比如擴充卡,用來适配老的接口,比如外觀模式,用來隐藏各個子系統,各個子產品的協作細節。

不過外觀模式卻不一定都是在後期才發力的:

1,前期如果系統比較複雜,在系統規劃的時候,就會有意識的對系統分層,為上層子產品提供一些進階的api。

2,在系統的中期呢,開發過程中,發現子系統越來越複雜,也可以提供類似的操作。

3,在系統後期,子產品越來越多,功能越來越複雜,還有曆史原因,外觀模式就更加有用了,畢竟,有一個簡單易用的API,比手動調用各個系統的邏輯那簡直是不要太舒服!

為什麼後期不重構?而是要做這些修修補補的工作呢?

舉個例子,房子上有棵樹,你覺得這棵樹很礙事,就把樹給幹掉了,因為你以為,是房子上面長的,結果呢?特麼是樹把房子吊着的!類似的坑實在是太多了,是以,重構需謹慎,且構且珍惜。

當然不是說重構不好,而是要綜合考量各方面的因素,而且,重構也用得上這些啊,畢竟,重構不是重寫...(诶,重寫好像也要用)

擴充卡模式

先說說它是幹嘛的,用通俗一點的話來講就是,VGA轉DVI,2線插頭轉3線插頭...廢話不多說,上個圖就知道了

headfirst設計模式(8)—擴充卡模式與外觀模式

什麼?大家很想看個例子?那麼我就來一個例子吧,就舉一個小火雞變成小鴨子的故事吧

先看看鴨子接口(對應Target)

/**
 * 鴨子接口
 */
public interface Duck {
    /**
     * 鴨叫
     */
    void quack();

    /**
     * 飛行
     */
    void fly();
}      

然後看一下火雞的接口和實作類(對應Adaptee)

/**
 * 火雞接口
 */
public interface Turkey {
    /**
     * 火雞叫
     */
    void gobble();

    /**
     * 飛行
     */
    void fly();
}

/**
 * 野火雞
 */
public class WildTurkey implements Turkey {
    public void gobble() {
        System.out.println("咯咯");
    }
 
    public void fly() {
        System.out.println("我在飛,雖然我飛的很近");
    }
}      

首先可以看出,它們的之間有一些共同之處,都有叫聲,都可以飛行,這個也是适配的前提,有共同點!

如果沒有共同點,是不是去隔壁的其他設計模式看看?

OK,接下來開始适配操作

火雞擴充卡(Adapter)

/**
 * 火雞擴充卡
 */
public class TurkeyAdapter implements Duck {
    Turkey turkey;//持有一個火雞對象
 
    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }

    /**
     * 鴨叫
     */
    public void quack() {
        turkey.gobble();
    }

    /**
     * 飛行
     */
    public void fly() {
        //适配的時候,這裡模拟飛行5次
        for(int i= 0; i < 5; i++) {
            turkey.fly();
        }
    }
}      

擴充卡的邏輯也很簡單

首先,實作Duck接口,要讓Client能夠調用,那麼首先得長得和别人一樣啊

其次,持有一個真正的處理對象,然後再根據Duck接口來進行适配,比如這裡,quack接口,就直接調用Turkey#gobble(),而fly()可能是因為某種神秘力量,需要火雞飛行的距離和鴨子一樣遠,是以需要手動去适配,在這裡添加了适配的代碼

最後,擴充卡的作用就是把一個類轉換成另外一個類,轉換的時候可能需要一些邏輯上的處理,讓它能符合使用者的期待

測試下是不是成功的僞裝了呢

public class DuckClient {
    public static void main(String[] args) {

        //初始化一隻火雞
        WildTurkey turkey = new WildTurkey();
        //僞裝成一隻鴨子
        Duck duck = new TurkeyAdapter(turkey);

        System.out.println("鳴叫:");
        duck.quack();

        System.out.println("------------------");

        System.out.println("飛行:");
        duck.fly();
    }
}      

結果:

headfirst設計模式(8)—擴充卡模式與外觀模式

擴充卡模式模式确實很簡單,但是确實也很實用,優點很明顯,可以将目标類和适配者解耦,不需要改動原來的結構(新增了Adapter來封裝了适配的邏輯),但是建議不要在系統設計階段就盲目的使用它,增加系統的複雜度

外觀模式

這個就更簡單了,例子我可以舉一堆,比如說,酒店前台的小姐姐,餐廳前台的小姐姐,醫院的小姐姐...

核心思想:為子系統們提供一套通用的對外接口(進階API)

為什麼會有這樣的需求呢?

各個子系統在設計過程中,或者在實際使用的過程中會發現,有一些通用的步驟,對于更加高的調用層來說,它們其實不需要知道底層是通過哪些步驟來實作的,更多的是,以一個統一的接口來調用。

比如,在想在家裡搞一個家庭影院,需要以下步驟:

1,燈光不能太亮,亮度需要調低到10

2,需要打開投影機,并且要調整到寬屏模式

3,音響需要調整成環繞立體音,音量設定成5

4,打開DVD開始播放

代碼如下:

燈光:

headfirst設計模式(8)—擴充卡模式與外觀模式
headfirst設計模式(8)—擴充卡模式與外觀模式
/**
 * 影院燈光
 */
public class TheaterLights {
    String description;

    public TheaterLights(String description) {
        this.description = description;
    }

    public void on() {
        System.out.println(description + " 打開");
    }

    public void off() {
        System.out.println(description + " 關閉");
    }

    public void dim(int level) {
        System.out.println(description + " 亮度調節到:" + level  + "%");
    }

    public String toString() {
        return description;
    }
}      

View Code

投影儀:

headfirst設計模式(8)—擴充卡模式與外觀模式
headfirst設計模式(8)—擴充卡模式與外觀模式
/**
 * 投影儀螢幕
 */
public class Screen {
    String description;

    public Screen(String description) {
        this.description = description;
    }

    public void up() {
        System.out.println(description + " 上升");
    }

    public void down() {
        System.out.println(description + " 下降");
    }


    public String toString() {
        return description;
    }
}
/**
 * 投影儀
 */
public class Projector {
    String description;
    DvdPlayer dvdPlayer;
    
    public Projector(String description, DvdPlayer dvdPlayer) {
        this.description = description;
        this.dvdPlayer = dvdPlayer;
    }
 
    public void on() {
        System.out.println(description + " 打開");
    }
 
    public void off() {
        System.out.println(description + " 關閉");
    }

    public void wideScreenMode() {
        System.out.println(description + " 調整到寬屏模式");
    }

    public void tvMode() {
        System.out.println(description + " 調整到tv模式");
    }
  
    public String toString() {
            return description;
    }
}      

View Code

音響:

headfirst設計模式(8)—擴充卡模式與外觀模式
headfirst設計模式(8)—擴充卡模式與外觀模式
/**
 * 音響
 */
public class Amplifier {
    String description;

    public Amplifier(String description) {
        this.description = description;
    }
 
    public void on() {
        System.out.println(description + " 打開");
    }
 
    public void off() {
        System.out.println(description + " 關閉");
    }

    //立體聲
    public void setStereoSound() {
        System.out.println(description + " 立體聲模式");
    }

    //環繞聲
    public void setSurroundSound() {
        System.out.println(description + " 環繞聲模式");
    }
 
    public void setVolume(int level) {
        System.out.println(description + " 調整音量到: " + level);
    }

    public String toString() {
        return description;
    }
}      

View Code

DVD播放器:

headfirst設計模式(8)—擴充卡模式與外觀模式
headfirst設計模式(8)—擴充卡模式與外觀模式
/**
 * DVD播放器
 */
public class DvdPlayer {
    String description;
    int currentTrack;
    Amplifier amplifier;
    String movie;
    
    public DvdPlayer(String description, Amplifier amplifier) {
        this.description = description;
        this.amplifier = amplifier;
    }
 
    public void on() {
        System.out.println(description + " 播放");
    }
 
    public void off() {
        System.out.println(description + " 關閉");
    }

    public void play(String movie) {
        this.movie = movie;
        currentTrack = 0;
        System.out.println(description + " 播放 \"" + movie + "\"");
    }

    public String toString() {
        return description;
    }
}      

View Code

不重要的代碼就折疊了,免得難得看,不使用外觀模式,需要調用一堆代碼:

/**
 * 不使用外觀模式
 */
public class Client {
    public static void main(String[] args) {
        Amplifier amp = new Amplifier("Top-O-Line 揚聲器");
        DvdPlayer dvd = new DvdPlayer("Top-O-Line DVD播放器", amp);
        Projector projector = new Projector("Top-O-Line 投影儀", dvd);
        TheaterLights lights = new TheaterLights("客廳燈");
        Screen screen = new Screen("投影儀銀幕");

        System.out.println("準備看電影...");
        lights.dim(10);
        screen.down();
        projector.on();
        projector.wideScreenMode();
        amp.on();
        amp.setSurroundSound();
        amp.setVolume(5);
        dvd.on();
        dvd.play("奪寶奇兵");
    }
}      

使用外觀模式,一行解決:

/**
 * 使用外觀模式後的測試類
 */
public class FacadeClient {

    private static HomeTheaterFacade HOME_THEATER;
    static{
        Amplifier amp = new Amplifier("Top-O-Line 揚聲器");
        DvdPlayer dvd = new DvdPlayer("Top-O-Line DVD播放器", amp);
        Projector projector = new Projector("Top-O-Line 投影儀", dvd);
        TheaterLights lights = new TheaterLights("客廳燈");
        Screen screen = new Screen("投影儀銀幕");

        HOME_THEATER = new HomeTheaterFacade(amp, dvd, projector, screen, lights);
    }

    public static void main(String[] args) {
        //看電影
        HOME_THEATER.watchMovie("奪寶奇兵");
    }
}      

我擦?咋還是這麼多行?

static塊裡面的代碼是初始化代碼,一般使用spring,都是依賴注入的東西,其實調用就一行:

HOME_THEATER.watchMovie("奪寶奇兵");      

一鍵解決就是爽啊,如果說對比的話,相當于,去網上買了個床,小哥送來的是一堆零件讓你組裝,和小哥送來就是一張組裝好了的床啊!

但是能夠一鍵解決的,更多的是一些通用的操作,比如說,例子中,燈光不能太亮,你想把它調到5,不想用預設的10,,那麼可能就隻能自己寫一遍外觀模式封裝的邏輯了。

那麼這裡就有個問題了,能不能重載方法,讓它支援可以自定義燈光亮度這個參數呢?對于這個我隻能說,要看業務需求了,如果100個人裡面隻有1個人用,那麼對于系統産生的複雜度可能比 産生的價值高,反過來,可能就需要去實作。

但是,如果這種需求越來越多,系統變得越來越複雜,那外觀模式還是一個簡單可愛的小姐姐嗎?如果不實作,就無法達到隐藏子系統複雜度的痛點,如果實作,就會産生新的API調用的複雜度,我終于知道為啥我特麼還在學習設計模式了...

說了這麼多,說說它的優缺點吧

優點:

1,對客戶屏蔽了子系統元件使用起來門檻更低。

2,實作了子系統與客戶之間的松耦合關系。

3,雖然提供了通路子系統的統一入口,但是并不影響使用者直接使用子系統類。

缺點:

1,通過外觀類通路子系統時,減少了可變性和靈活性。

2,在新的子系統加入,或者子系統接口變更時,可能需要修改外觀類或用戶端的源代碼,違背了“開閉原則”。