跳到主要內容

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

留言

這個網誌中的熱門文章

整理設計模式

        依據 GOF 的書,可以將經典的設計模式分為以下三類:生成、行為、結構。 生成模式 :牽涉到 將物件實體化 。這類模式都提供一個方法,將客戶從所需要實體化的物件中鬆綁出來。 獨體模式 (Singleton Pattern) 工廠方法模式 (Factory Method Pattern) 抽象工廠模式 (Abstract Factory Pattern) 建立者模式 (Builder Pattern) 原型模式 (Prototype Pattern) 結構模式 :讓你 合成類別或物件到大型的結構 。 裝飾者模式 (Decorator Pattern) 轉接器模式 (Adapter Pattern) 表象模式 (Facade Pattern) 合成模式 (Composite Pattern) 代理人模式 (Proxy Pattern) 橋接模式 (Bridge Pattern) 享元模式 (Flyweight Pattern) 行為模式 :模述 類別和物件如何互動 ,以及 各自的責任 。 策略模式 (Strategy Pattern) 觀察者模式 (Observer Pattern) 命令模式 (Command Pattern) 樣板方法模式 (Template Method Pattern) 反覆器模式 (Iterator Pattern) 狀態模式 (State Pattern) 責任鏈模式 (Chain of Responsibility Pattern) 解譯器模式 (Interpreter Pattern) 中介者模式 (Mediator Pattern) 備忘錄模式 (Memento Pattern) 訪問者模式 (Visitor 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 份牛奶)。這時候裝飾者模式就能上場啦。在介紹裝飾者模式前, 先說明其設計守則: 類別應該開放, 以便擴充 ; 應該關閉, 禁止修改。         我們的目標是允許類別容易擴充, 在不修改現有程式碼的情形就能搭配新的行為。這樣的設計具有彈性, 可以接受新功能以達到改變需求的目的。這看起來好像有點矛盾, 但是的確有一些技術可以在不直接修改程式碼的情形下進行擴充, 如裝飾者模式。         這時候應該有人會問: 那是不是以後我的專案架構設計都遵循這個守則就是好設計了 ? 答案是不太可能, 也沒這必要, 就算做得到, 也可能是浪費, 容易導致程式碼複雜且難以理解。只需小心選擇哪些部分未來會擴充, 這些部份遵循這個設計守則即可。         接下來正式介紹裝飾者模式的定義:  裝飾者模式動態地將責任加諸於物件上。若要擴充功能,裝飾者模提供了比繼承更有彈性的選擇 Attach additional responsibilities to an object dynamically. Decorators provide a flexible alt