設計模式—建造者模式 (Builder Design Pattern)

客製化種種零件以打造複雜物件

Wenchin
Wenchin Rolls Around
11 min readJun 30, 2020

--

建造者模式在幹嘛?

建造者 (Builder) 模式將物件的「建構」與「表示」分離,隱藏並封裝建構過程的細節。它讓我們可以將物件本身拆解成不同的元件,一步一步建造每一部分,最後產生出我們想要的複雜物件。

Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.

為什麼要用建造者模式?

要解決的問題為:

  1. 拖太長的建構子 (telescoping constructor)
  2. 建構子內不需要的參數必須放 null

以下我們先用一個蓋房子的例子,來觀察這些問題。

Photo by Christopher Burns on Unsplash

蓋一間房子

假設我要創造一個房子的物件,那最簡單粗暴的方法就是定義一個物件 House、賦予它房子該有的屬性 (property):窗戶數、門數、房間數,並使用建構子 (constructor) 來帶入這些參數。

以 pseudocode 表示:

蓋許多間不同的房子

今天假設我想要創建不同的房子:有花園的房子、有泳池的房子、有車庫的房子等等呢?

簡單,我們都知道可以把 House 變成 base class,再讓其他子類別繼承它,打造不同的房子。

這時為了記錄這些房子多出來的功能,我的 House 物件的屬性就要擴增成:窗戶數、門數、房間數、有無花園、有無泳池、有無車庫。

那假設我這個 House 物件要給更多房子用:有雕像的房子、有健身房的房子、有爐火的房子等等,那這物件的參數會拖得很長。這顯然不是一個好的解法。

因為當我要創造一個房子時,我很難一目瞭然每個建構子代表的意義:

而且如果今天我可能有一系列的房子沒有爐火管線、也禁止蓋健身房,那可能對於這些房子我都要把某些參數設為 null,增加更多的混亂:

Builder: 客製化蓋房子的種種零件

建造者模式用一種 configuration 的方式,拆解每個元件建造的過程,避免有拖得很長的建構子,使建構的意圖清晰許多。

因此剛剛那些房子,實現建造者模式的程式碼可能會長得像這樣:

或是這樣:

以下我們用另一個例子詳細說明,並在 C# 實現這兩種方式。

怎麼實現建造者模式?

開一間水上活動工作室

假設今天你擁有 SUP、獨木舟、浮潛、衝浪……的器材和專業,打算開一個水上活動工作室,設計不同的活動來銷售。

藉由建造者模式,我們可以開始設計不同的課程。

使用建造者模式的方法有兩種:

方式一:透過 Client、Director、Builder 和 Product 形成的建造者模式

Source: Refactoring Guru — Design Patterns: Builder

Builder: 創建抽象建造者 ITripBuilder,僅宣告打造水上活動行程的步驟,不實作這些方法

Concrete Builder: 建立實體建造者 KayakTripBuilder, SupTripBuilder,實現 ITripBuilder 的各式設定

這邊以 SupTripBuilder 舉例,我們暫且假設 SUP 行程的價格都是 $3,000、行程時間都是 5 小時……,僅有地點是變動的:

Director: 建立 TripDirector,讓它呼叫 KayakTripBuilder, SupTripBuilder 以建立行程的各個部分,並宣告一套流程按順序 (從 SetSalesContext, SetDestination, SetPrice… 接下去) 來建造複雜物件

Product: 建立一個物件 Trip 收整行程內容

這邊簡化的用 List 紀錄行程內容的字串,並直接 yield return 這些內容:

Client: 建構完以上物件,我們可以在Program.cs呼叫 TripDirector 開始打造行程

Console output:

方式二:透過靜態內部類方式實現建造者模式

上面的假設是 SUP 行程價格、困難度、時間……都相同,僅地點有差異,所以 TripDirector 的參數只有一個。

然而今天可能行程一多,針對同樣都是 SUP(或同樣都是獨木舟)的行程,不同地點我們希望也做不同價格、困難度、時間……的設定。

這時候用第二種實現方法,會更加的彈性。

這種方式使用更加靈活,更符合定義。內部有複雜物件的預設實現,使用時可以根據使用者需求自由定義更改內容,並且無需改變具體的構造方式。就可以生產出不同複雜產品

在我們的例子,就是讓 Trip 本身可以直接設定傳入的參數,並且每個 Builder 方法都回傳 this,以達到 chaining 的效果。

Builder: 創建抽象建造者 ITripBuilder,僅宣告打造水上活動行程的步驟,注意回傳型別也都是 ITripBuilder

Concrete Builder: 建立實體建造者 KayakTripBuilder, SupTripBuilder,實現 ITripBuilder 的方法

這邊以 SupTripBuilder舉例,我們把 SetDestination, SetPrice… 等步驟都交給 Trip 這個最後要完工的物件去進行:

Product: 建立一個物件 Trip 收整行程內容

和第一個方法不同,這個方法直接在物件中做 SetDestination, SetPrice… 等步驟。最後一樣全部整理成字串丟到一個 List 裡面:

Client: 建構完以上物件,我們可以在Program.cs呼叫 Builder 並用 chaining 的方式打造行程

用這個方法可以更彈性就是因為順序(先設定地點還先設定價格?)、屬性多寡(要不要這個行程暫不設定行銷內容跟活動敘述?)都可以直接在建造 Trip 時決定。並且因為 TripBuilder 的方法都是回傳該 TripBuilder (this),所以可以一路 chain 下去:

Console output:

GitHub repo

方式一:透過 Client、Director、Builder 和 Product 形成的建造者模式

方式二:透過靜態內部類方式實現建造者模式

Reference

--

--