跳到主要內容

表象模式 (Facade Pattern)

        某天你心血來潮,想在家裡準備家庭劇院組。你為此做了一番研究,找來了覺得適合的播放器、投影機、螢幕、音響等等的設備,當你要準備開始看電影時,你要做哪些事呢?
  1. 將燈光調暗
  2. 開啟螢幕
  3. 打開投影機
  4. 設定投影機輸入模式
  5. 打開音響
  6. 設定音響音量
  7. 打開播放器
  8. 開始播放


        將這些動作寫成程式碼的話:
// 將燈光調暗到 10% 的亮度
lights.dim(10);

screen.on();

projecter.on();
projecter.setInput(player);

// 開啟喇叭
amp.on();
amp.setVolumn(5);

player.on();
player.play();
        這樣看起來做的事好像沒很多,但是當看完電影後,要把所有設備關掉要怎麼做?全部反向做一次嗎?假如要聽音樂而已,也要這麼麻煩嗎?未來升級新設備時,還要重新學習操作流程嗎?

        這時候最直覺的想法一定是:把這些事包成一個 function 就好啦。這就是表象模式的精神所在,將一個或數個類別複雜的一切都隱藏起來,只露出美好的表面(就是簡化介面啦)。

        使用表象模式,可以將一個複雜的次系統,變得容易使用。表象類別提供更合理的介面,來簡化原先的複雜介面。假如不想用表象介面時,還是可以直接操作次系統。

        在這邊特別說明一下,表象模式跟轉接器模式雖然都是用來封裝類別,但是他們的目的卻是不同的。轉接器模式的目的是改變介面以符合客戶的期望。而表象模式是提供次系統一個簡化的介面
public class HomeTheaterFacade {

    // 這就是合成
    // 會用到的次系統元件全都在這裡
    Amplifier mAmp;
    Player mPlayer;
    Projector mProjector;
    TheaterLights mLights;
    Screen mScreen;

    // 透過建構式將次系統元件記錄下來
    public HomeTheaterFacade(Amplifier amp,
                            Player player,
                            Projector projector,
                            TheaterLights lights,
                            Screen screen)
    {
        mAmp = amp;
        mPlayer = player;
        mProjector = projector;
        mLights = lights;
        mScreen = screen;
    }

    public void watchMovie(String movie)
    {
        System.out.println("Ready to watch a movie.");

        // 這邊就是將前面說的動作依序處理
        // 注意每件工作都是委由次系統的元件處理
        mLights.dim(10);
        mScreen.on();
        mProjector.on();
        mProjecter.setInput(mPlayer);
        mAmp.on();
        mAmp.setVolumn(5);
        mPlayer.on();
        mPlayer.play();
    }

    public void endMovie()
    {
        System.out.println("Ready to shut down theater.");

        // 關閉次系統中的所有元件
        mLights.on();
        mScreen.off();
        mProjector.off();
        mAmp.off();
        mPlayer.off();
    }
}
        程式碼很容易理解,不需多加說明,就只是把次系統的工作封裝起來而已。以下正式介紹表象模式的定義:

表象模式提供了一個統一的介面,用來存取次系統中的一群介面。表象定義了一個較高層次的介面,讓次系統更容易使用。
Provide a unified interface to a set of interfaces in a subsystem. Façade defines a higher-level interface that makes the subsystem easier to use.


        從類別圖更容易感受到表象模式「簡化」的功用。客戶只需要面對單一個介面,而不用去跟次系統中複雜的類別打交道。了解這個模式後,可以再順便學習一個設計守則,認識極少化守則 (Least Knowledge):只和你的密友談話

        這個設計守則的意思是,在設計一個系統時,不管是任何物件,都要注意它所互動的類別有哪些,以及如何互動。此守則是希望在設計中,不要有太多類別綁在一起,以免修改系統一部份會影響其它部份。如果類別之間互相依賴,就會變成一個易碎的系統,需要花許多時間維護。

        那要怎麼做到極少化守則呢?有幾個方向能參考。以某個物件而言,此物件的方法定義內,只能引用物件的某些方法,而這些方法必須屬於:
  • 該物件本身
  • 當作方法的參數傳進來的物件
  • 此方法建立或實體化的物件
  • 物件的任何元件
        這樣看可能不容易懂,我們以程式碼來說明:
public float getTemp()
{
    // 從氣象站取得了溫度計物件,
    // 再從該物件取得溫度,
    // 這樣就沒遵守認識極化守則
    Thrmometer thermometer = station.getThermometer();
    return thermometer.getTemperature();
}

public float getTemp()
{
    // 氣象站加入取得溫度的方法,
    // 減少所依賴的類別數目
    // 這樣就有遵守認識極化守則
    return station.getTemperature();
}
public class Car {

    // 這是類別裡的一個元件,
    // 可以呼叫他的方法
    private Engine mEngine;

    public void start(Key key)
    {
        // 在方法裡建立物件,
        // 可以呼叫他的方法
        Doors doors = new Doors();

        // key 是被當作參數傳進來,
        // 可以呼叫 key 的方法
        boolean authorized = key.turns();

        if(authorized)
        {
            // 可以呼叫物件所屬元件的方法
            engine.start();

            // 可以呼叫同一物件的 local method
            updateDashboardDisplay();

            // 可以呼叫方法內建立的物件的方法
            doors.lock();
        }
    }

    public updateDashboardDisplay()
    {
        // update display...
    }
}

        認識極少化守則跟其他守則一樣,並沒有要強制遵守,而是要考量所有的因素後再決定是否要遵守。雖然認識極少化守則減少了物件之間的相依性,可以減少軟體維護成本,但也會導致更多的「包裝」類別被製造出來以處理物件之間的構通。這可能導致複雜度及開發時程增加,並降低執行期的效率。

參考資料:

        深入淺出設計模式(Head First Design Patterns)

留言

這個網誌中的熱門文章

訪問者模式 (Visitor Pattern)

        假設你設計一個系統,其中會有一些相似類別,類別中都有某些方法內容相似,但還是需要判斷目前要做事的是哪個類別才能呼叫對應的適當類別。通常遇到這種情情,在 Java 中最直接的做法就是使用 instanceof 關鍵字來判斷,如以下的簡單範例: public interface CarComponent { public void printMessage(); } public class Wheel implements CarComponent { @Override public void printMessage() { System.out.println("This is a wheel"); } // 這是 Wheel 跟 Engine 不同的方法 public void doWheel() { System.out.println("Checking wheel..."); } } public class Engine implements CarComponent { @Override public void printMessage() { System.out.println("This is a engine"); } // 這是 Wheel 跟 Engine 不同的方法 public void doEngine() { System.out.println("Testing this engine..."); } } public class Car { private List mComponents; public Car() { mComponents = new ArrayList<carcomponent>(); } // 有些時候我們還是需要針對不同類別去做不同的事情 public void setComponent(CarCompon...

裝飾者模式 (Decorator Pattern)

        假如你有一間飲料店, 目前只有賣幾種咖啡。因為生意很好, 因此想更換菜單…         以下是目前菜單的類別圖:         簡單說明此類別圖, cost() 是抽象的, 子類別要實作自己的 cost() 來告知飲料的價格。         買咖啡時, 也可要求要加料, 例如牛奶(Milk)、摩卡(Mocha,就是巧克力口味)。這樣的新類別要如何設計呢 ? 看起來是不能直接新增所需的子類別, 例如 EspressoWithMilk, EspressoWithMilkAndMocha, DarkRoastWithMilk, DarkRoastWithMilkAndMocha… 這樣加下去, 日後飲料跟配料越來越多時, 類別也就越多, 這實在不是個好設計。         換個方式設計呢, 在 Beverage 裡面加入所有的配料如何 ? 這樣好像也不太好, 未來要是配料有更動, Beverage 程式碼就要重寫, 而未來要是有新口味的飲料時, 有些配料就不太合理 ( 薑茶加摩卡 ? ), 更麻煩的是, 無法應付機車的客人 (例如要加 3 份牛奶)。這時候裝飾者模式就能上場啦。在介紹裝飾者模式前, 先說明其設計守則: 類別應該開放, 以便擴充 ; 應該關閉, 禁止修改。         我們的目標是允許類別容易擴充, 在不修改現有程式碼的情形就能搭配新的行為。這樣的設計具有彈性, 可以接受新功能以達到改變需求的目的。這看起來好像有點矛盾, 但是的確有一些技術可以在不直接修改程式碼的情形下進行擴充, 如裝飾者模式。         這時候應該有人會問: 那是不是以後我的專案架構設計都遵循這個守則就是好設計了 ? 答案是不太可能, 也沒這必要, 就算做得到, 也可能是浪費, 容易導致程式碼複雜且難以理解。只需小心選擇哪些部分未來會擴充, 這些部份遵循這個設計守則即可。         接下來...

狀態模式 (State Pattern)

        如果今天你要設計一台如下圖的糖果機,你會怎麼設計呢?         有上過資訊相關課程的人,應該不難從上圖聯想到 狀態圖 ,上圖中每個圓圈都是一個狀態,而每個箭頭就代表狀態的轉換。有了這個概念後,把它轉成程式就不難了: public class CandyMachine { // 以下四個值表示糖果機會用到的狀態 final static int SOLD_OUT = 0; final static int NO_COIN = 1; final static int HAS_COIN = 2; final static int SOLD = 3; // 需要有一個變數來記錄目前的狀態 // 初始設為賣完, 因為一開始機器裡沒糖果 int mState = SOLD_OUT; // 也要有另一個變數記錄目前剩多少顆糖果 int mCount = 0; public CandyMachine(int count) { mCount = count; // 機器內有糖果的話就跳到沒投錢的狀態, // 表示糖果機在等人投錢 if(mCount > 0) { mState = NO_COIN; } } // 當投錢時會執行這個方法 public void insertCoin() { if(mState == HAS_COIN) { System.out.println("You can't insert another coin"); } else if(mState == NO_COIN) { mState == HAS_COIN; System.out.println("You inserted a coin"); } ...