Builder Pattern

Builder 可以讓我們將物件的「建構」和「表示」相互分離。這讓我們可以用同一份建構碼,但用不同的表示方式來創建不同的物件。

我相信看完這個定義沒有人會了解所以到底 Builder 是幹嘛的!以我的理解,Builder 的目的其實就是將過於複雜的建構過程拆解為一個一個小而簡單的建構步驟,最後生成我們想要的那個物件。

為什麼需要 Builder?

那麼重要的問題來了,為什麼我們需要 Builder 呢?前面說了,Builder 可以解決的是過於複雜的物件建構過程。那什麼叫做「複雜的建構過程」呢?

複雜的建構過程就是當一個物件在建構時,所需要的參數太多,而且有些參數可有可無時,這樣就是一個複雜的建構過程。

舉例來說,今天假設我們有一個物件 School,他長這樣:

class School
{
    int teacher;          // 必須
    int student;          // 必須
    int basketballCourt;  // 可有可無
    int soccerCourt;      // 可有可無
    int playGround;       // 可有可無
}

我們設定一個學校必須要有老師和學生,而其餘的則是非必需的。在還沒學過 Builder 以前,我們需要寫下四種建構子,分別是:

School(int t, int s);
School(int t, int s, int bc);
School(int t, int s, int bc, int sc);
School(int t, int s, int bc, int sc, int pg);

這是為了讓使用者可以依照他們的需求建構出所需要的 School 物件。這樣是可以行得通的,但是問題也非常明顯。每多加一個參數,即使是非必要的參數,我們也需要多寫一個建構子。而且使用者在建構物件

時,要搞清楚每一個參數代表的意義。如果參數的名字長得很像,而且參數的資料型態也都一樣的話,就很容易將參數的位置搞錯,而這樣的小錯誤在大型的程式碼中就會很難被發現。

Builder 就是為了要解決這樣問題而產生的。我們先來看一下在做出 Builder 後這樣的程式碼會如何被改善

School school = School::Builder(2, 10)
                .setBasketballCourt(2)
                .setSoccerCourt(1)
                .setPlayGround(1)
                .build();

是不是變得非常清楚呢!使用者可以明確知道必要參數是什麼,也知道當前設置的參數代表的意義是什麼!那所以我們該如何實作 Builder 呢?

如何實作?

首先我們在 School 中建立 Builder 這個物件,然後將 School 中的所有參數都複製到 Builder 中。注意,我們在這裡利用 struct 建立 Builder,這是因為我們之後需要用到 Builder 裡面的 api。

class School
{
public:
    static struct Builder
    {
    private:
        int teacher;
        int student;
        int basketballCourt;
        int soccerCourt;
        int playGround;
    };

private:
    int teacher;
    int student;
    int basketballCourt;
    int soccerCourt;
    int playGround;
};

接著,我們建立 Builder 的建構子,這個建構子需要所有建構 School 這個物件的必要參數。

static struct Builder
{
    Builder(int t, int s)
    {
        teacher = t;
        student = s;
    }
};

接著,我們在 Builder 中建立 api,這些 api 讓我們可以設置那些非必要參數的。並且這些函數會回傳 Builder 型態,記住這一個步驟是非常重要的!因為這讓我們可以串接所有的 api 呼叫!否則 Builder 的使用就不會成功。這邊我們為了簡潔紙寫下一種:

Builder setSoccerCourt(int sc)
{
    soccerCourt = sc;
    return *this;
}

接下來,我們要在 Builder 中加入 build() 這個函數,這個函數會建構 School 這個物件並回傳。

School build()
{
    return new School(*this);
}

最後一步,我們要回去寫 School 這個物件的建構子。現在這個建構子就變得非常簡單了!只需要一個參數,也就是 Builder,並將 Builder 中設置好的參數傳給 School

School(Builder builder)
{
    teacher = builder.getTeacher();
    student = builder.getStudent();
    basketballCourt = builder.getBasketballCourt();
    soccerCourt = builder.getSoccerCourt();
    playGround = builder.getPlayGround();
}

最後我們的程式碼會長這樣:

class School
{
public:
    static struct Builder
    {
        Builder(int t, int s)
        {
            teacher = t;
            student = s;
        }

        Builder setBasketballCourt(int bc)
        {
            basketballCourt = bc;
            return *this;
        }

        Builder setSoccerCourt(int sc)
        {
            soccerCourt = sc;
            return *this;
        }

        Builder setPlayGround(int pg)
        {
            playGround = pg;
            return *this;
        }

        School build()
        {
            return School(*this);
        }
    private:
        int teacher;
        int student;
        int basketballCourt;
        int soccerCourt;
        int playGround;
    };

private:
    School(Builder builder)
    {
        // 需要另外實作 getters
        teacher = builder.getTeacher();
        student = builder.getStudent();
        basketballCourt = builder.getBasketballCourt();
        soccerCourt = builder.getSoccerCourt();
        playGround = builder.getPlayGround();
    }
    int teacher;
    int student;
    int basketballCourt;
    int soccerCourt;
    int playGround;
};

如何使用?

最後我們終於可以用我們寫出來的 Builder 啦!使用方法就像前面講到的

School school = School::Builder(2, 10)
                .setBasketballCourt(2)
                .setSoccerCourt(1)
                .setPlayGround(1)
                .build();

注意因為我們將 Builder 設置為 School靜態變數,因此我們需要利用 :: 來取用 Builder。至於為什麼我們要將它設為靜態變數呢?這是因為我們並不需要多個 Builder,每一個物件應該都只會需要一個 Builder,因為這個 Builder 的主要且唯一功能就是要建構對應的物件。

因為之前我們將 Builder 設置為 struct,所以我們可以自由取用各種 setter 來設置參數。也因為每個 setter 都會回傳 *this,所以我們可以直接串接每個函數呼叫。

結語

這樣我們就介紹完 Builder Pattern 啦!雖然知道了什麼是 Builder,但我們應該什麼時候使用它呢?什麼時候該用什麼時候可以不用呢?我想一個很好的指標就是:

如果一個物件所需要的參數有四或五個甚至更多,而且其中有些參數並不是必須的,那我們就可以考慮使用 Builder 來幫助簡化建構過程!