需求:要建一個
HttpRequest(或 SQL 查詢、UI 對話框)——它有兩三個必填、十來個可選欄位,還有「先設 header 再設 body」這種組裝順序。用一個巨型建構子會爆炸,用一堆 setter 又會讓物件中途處於半組好的壞狀態。怎麼設計?
🎯 Scenario
// 反例:telescoping constructor —— 參數一多就無法閱讀、無法辨識哪個是哪個
class HttpRequest(
url: String, method: String, headers: Map<String, String>,
body: String?, timeoutMs: Int, retries: Int, followRedirects: Boolean,
// …再過半年參數破十五,呼叫端 new HttpRequest("...", "GET", emptyMap(), null, 0, 0, false) 誰看得懂
)
需求關鍵字:同一種產物、很多可選部件、需要分步組裝、且要保證建好時是完整合法的。這是 Builder 的主場。
🧠 Intuition
TIP
先別看下面。分清兩件事:「要組出什麼」 和 「怎麼一步步組」。把逐步設定的過程收進一個專門的建造者,最後用一個 build() 一次產出不可變、且已驗證的成品——中途永遠不會有半殘物件外流。
⚖️ Reference Design
主解法 — Builder:分步設定,build() 一次成形
class HttpRequest private constructor(
val url: String, val method: String,
val headers: Map<String, String>, val body: String?, val timeoutMs: Int,
) {
class Builder(private val url: String) { // 必填走建構子
private var method = "GET"
private val headers = mutableMapOf<String, String>()
private var body: String? = null
private var timeoutMs = 30_000
fun method(m: String) = apply { method = m } // 可選走 fluent setter
fun header(k: String, v: String) = apply { headers[k] = v }
fun body(b: String) = apply { body = b }
fun build(): HttpRequest {
require(timeoutMs > 0) { "timeout must be positive" } // 在此集中驗證
return HttpRequest(url, method, headers.toMap(), body, timeoutMs)
}
}
}
val req = HttpRequest.Builder("https://api.x/orders")
.method("POST").header("Auth", token).body(payload).build()- 可讀:每個設定都有名字,呼叫端一眼看懂。
- 完整性:成品在
build()後才誕生,且已驗證——不像一堆 setter 會讓物件中途半殘。 - 不可變:建好的
HttpRequest沒有 setter,天然執行緒安全(呼應不可變性)。
變體:Director、DSL、與語言原生手段
- Director:當「組裝步驟的順序/組合」本身要重用(如「標準請求」「壓測請求」兩種配方),抽一個 Director 封裝步驟序列,Builder 只提供零件。
- Kotlin 具名 + 預設參數:欄位單純、無組裝順序、無跨欄位驗證時,
data class的具名參數就夠了——別為此硬開 Builder(YAGNI)。 - type-safe builder / DSL:Kotlin 的
apply/receiver lambda 可把 Builder 寫成 DSL(如html { head { ... } })。
Builder vs 其他 creational 的分界
- 和 Factory/Abstract Factory 的差別:工廠關心「建哪一種」(挑型別);Builder 關心「一步步把同一種的複雜實例組好」。
- 常見誤用:欄位很少也套 Builder(過度設計);或 Builder 可變狀態被共用重入。先問這物件真的複雜到需要分步組裝嗎?
🔑 Takeaways
- 「同種產物 + 多可選部件 + 分步組裝 + 建好即完整合法」→ 用 Builder:fluent 設定、
build()集中驗證並產出不可變成品。 - 步驟配方要重用 → 加 Director;欄位單純 → 用具名/預設參數就好,別硬上 Builder。
- 與工廠分界:工廠選「哪一種」,Builder 組「同一種的複雜實例」。
No notes yet — jot your takeaways or Q&A here.