跳到主要內容

建立者模式 (Builder Pattern)

        今天你要為一個遊樂園設計程式,讓客人可以自由選擇旅館及各種門票、餐廳訂位、或是其他特殊活動來建立自己的假期計畫,但你可能會遇到一些問題:每個客人的假期計畫都不一樣,如旅行天數不同,或是想住的旅館會不同,而且為了要滿足客製化假期內容的要求,你的設計可能會有很多建構子才能滿足需求:

public class Vocation {

    // 客人只要求天數,其他隨便排
    public Vocation(Date begin, Date end)
    {
        // ...
    }

    // 客人要求天數,還指定飯店
    public Vocation(Date begin, Date end, Hotel hotel)
    {
        // ...
    }

    // 客人指定天數,飯店,還有餐廳
    public Vocation(Date begin, Date end, Hotel hotel,
                    Restaurant restaurant)
    {
        // ...
    }

    // 可能還有其他可以讓客人選擇的建構子
}

        這樣對於客戶而言,他需要先知道全部的建構子有哪些,才能知道要選用哪個來建立適合自己的假期物件。但有時候我們提供的的建構子可能還是不符合客戶的要求,因此我們需要有一個有彈性的結構,可以把整個假期物件的產生過程封裝起來,而且客戶還可以不影響物件建立的步驟,建立出自己想要的假期物件,這就是這次要介紹的建立者模式!

        我們先來看看建立者模式的定義及類別圖吧

將一個複雜對象的建構和表現分離,使得同樣的建構過程可以產生出不同的表現
Separate the construction of a complex object from its representation so that the same construction process can create different representations.


        從類別圖及定義可以知道,Builder 就是定義物件產生的一個介面,通常會定義一系列的方法,通常是 setXXX / addXXX,可以讓使用者來更改想要的物件行為。物件建構的過程是隱藏的,使用者是碰不到的。因為上述兩點,才符合定義說的相同建構過程可以有不同表現。Director 就是實際上調用這個介面來產生物件的中介者,主要的工作是指導 Builder 產生出特定的物件,client 要自己調用 Builder 來取得物件當然也可以。ConcreteBuilder 就是 Builder 介面的實作,可以產生真正產品的類別。

        接著就來看看上述的範例使用了建立者模式的程式碼吧

// Builder 介面
// 實務上 Builder 會搭配 Fluent interface
// 讓程式更有可讀性
public interface VocationBuilder {

    // 指定假期開始時間
    public VocationBuilder setBeginDate(String date);

    // 指定假期結束時間
    public VocationBuilder setEndDate(String date);

    // 指定住哪間飯店
    public VocationBuilder setHotel(Hotel hotel);

    // 指定吃哪間餐廳
    public VocationBuilder setRestaurant(Restaurant restaurant);

    // 指定要玩哪些景點的門票
    public VocationBuilder setTicket(List tickets);

    // 要提供一個方法讓使用者能取得假期物件
    public Vocation create();
}

// 能產生三天假期規劃的 Builder, 實作上面的 Builder
// 這裡只是範例, 實際上可以依需求有不同實作
public class ThreeDayVocationBuilder implements VocationBuilder {

    // 保留使用者想要的客製化,
    // 就是使用者想要的 "表現"
    private String mBeginDate;
    private String mEndDate;
    private Hotel mHotel;
    private Restaurant mRestaurent;
    private List mTickets;

    @Override
    public VocationBuilder setBeginDate(String date);
    {
        mBeginDate = date;
        return this;
    }

    @Override
    public VocationBuilder setEndDate(String date)
    {
        // 這邊可以加上些判斷, 確認使用者傳入的參數
        // 確實是三天後,或是自動幫使用者調整
        mEndDate = date;
        return this;
    }

    @Override
    public VocationBuilder setHotel(Hotel hotel)
    {
        mHotel = hotel;
        return this;
    }

    @Override
    public VocationBuilder setRestaurant(Restaurant restaurant)
    {
        mRestaurant = restaurant;
        return this;
    }

    @Override
    public VocationBuilder setTicket(List tickets);
    {
        mTickets = tickets;
        return this;
    }

    @Override
    public Vocation create()
    {
        // 回傳真正的假期物件給使用者
        // 省略了 Vocation 的程式碼
        // 這邊只是範例, 實務上可能物件的產生很繁瑣
        return new Vocation(mBeginDate, mEndDate, mHotel,
                            mRestaurent, mTickets);
    }
}

// 使用上可以這樣使用 Builder
VocationBuilder builder = new ThreeDayVocationBuilder();
builder.setBeginDate("2018/01/01");
builder.setEndDate("2018/01/03");
builder.setHotel(someHotel); // 懶得寫 hotel 物件
Vocation vocation = builder.build() // 這樣就能取得 Vocation
    
// 也能搭配使用 Fluent interface
Vocation vocation = new ThreeDayVocationBuilder()
                        .setBeginDate("2018/01/01")
                        .setEndDate("2018/01/03")
                        .setHotel(someHotel)
                        .create()

        看到這裡,可能有些人會覺得,同樣都是建立物件,那直接 Abstract Factory 來建立物件不就好了?這裡要注意的是, Builder 著重在隱藏複雜物件生成的步驟且生成的物件(通常是複雜物件) 彼此會有「部份」(Part of)的概念。而 Abstract Factory 則是著重在管理有關聯性的物件,但這些物件不會有「部份」(Part of)的概念。實務上這個模式還滿常被使用到的,如 JAVA SDK 裡的 StringBuilderStringBuffer,以及 Android SDK 裡的 AlertDialog.Builder,有興趣的人可以參考看看。

       最後來說說總結吧,建立者模式的特點就是將複雜物件的產生過程隱藏起來,使用者無法碰到,且允許物件用多個步驟建立 (跟 Factory Method 只有一個步驟不同),因為它的特性,因此經常用來建立合成結構。但對於使用者而言,要是不知道有哪些 setXXX() 方法可以用,也無法建立出想要的物件,這是要注意的地方。

參考資料:

    深入淺出設計模式(Head First Design Patterns)
    白話 Design Pattern (四) Builder pattern

留言

這個網誌中的熱門文章

訪問者模式 (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...

狀態模式 (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"); } ...