跳到主要內容

建立者模式 (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...

裝飾者模式 (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"); } ...