跳到主要內容

表象模式 (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...

解譯器模式 (Interpreter Pattern)

        解譯器模式簡單來說就是 把一句有特殊規則的語句,透過解釋器將它真正的意思表現出來 。相信有學過 Context-free grammar (CFG),Backus–Naur form (BNF),或是 Compiler 相關程的人會比較了解這個模式 (不是我,我早就忘光了…)。不知道上面術語的人,還是可以透過接下來的介紹來稍微了解這個模式。先來看一下這個模式的正式定義及類別圖: 定義一個語言與其文法,使用一個解譯器來表示這個語言的敘述 Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language Context 通常是指待解譯的語句 AbstractExpression 是所有規則都要實作的介面 TerminalExpression 是指無法再展開的規則,算是最小單位的規則 NonterminalExpression 是指可以再展開的規則,可以展開成 NonterninalExpression 和 TerminalExpression 的組合         從類別圖可以看到,語法可能可以一直展開,這時就可以用語法樹來表示,而語法樹以程式來表達的話,就可以使用 合成模式 。而通常比較正式的語法會用 BNF 來表示,如下: expression ::= plus | minus | variable | number plus ::= expression expression '+' minus ::= expression expression '-' variable ::= 'a' | 'b' | 'c' | ... | 'z' digit = '0' | '1' | ... | '9' number ::= digit | digit number 而 每個語法都會定義一個類別 ,如 pl...

代理人模式 (Proxy Pattern)

        代理人模式如同字面上的意思,就是做事情時(如取得某些資料),是透過代理人,而不是直接跟提供資料的物件構通。先來看看此模式的正式定義及類別圖: 代理人模式讓某個物件具有一個替身,藉以控制外界對此物件的影響。 Provide a surrogate or placeholder for another object to control access to it.         從類別圖可以看到, Subject 是共用的介面,可以讓客戶 將 Proxy 物件視為 RealSubject 物件 來處理。RealSubject 是真正做事的物件,被 Proxy 代理, Proxy 可以控制 RealSubject 的存取 。Proxy 持有 RealSubject 的參考,客戶和 RealSubject 的互動都要透過 Proxy,在某些情形下這樣的限制是必要的,如 RealSubject 是遠端物件, RealSubject 建立成本高等等。         此模式有許多種變形,都是依據上面的原則而發展出來的。以下介紹  深入淺出設計模式(Head First Design Patterns) 裡面提到的一些應用方式:         1. 遠端代理人(Remote) :算是最常使用到的應用方式。在網路中或是跨 Process 的各種程式,不可能直接存取(assess) 不同程式間的物件,因此就需要遠端代理人。 Java RMI  和 Android AIDL  都算是遠端代理人的實踐。         2. 虛擬代理人(Virtual) :代理的對象是建立很花費資源的物件。當物件建立前和建立中,由虛擬代理人扮演代理的角色。物件建立完成後,代理人就會將讓求直接轉給物件。以下為簡單範例程式: // Proxy 實作跟 RealSubject 共同的介面 Icon public class ImageProxy implements Icon { // 這是我們的 RealSubje...