一個物件有一堆可選參數與組裝步驟,怎麼建得乾淨?

Medium★★★★Studied

FromDesign Patterns (GoF)大話設計模式

Concepts practiced🏭 建造者 (Builder)📏 簡單設計三原則 (DRY / KISS / YAGNI)

Convention: read the requirements & constraints first, sketch your own design, then expand the reference to compare.

📚 From the Books

一個物件有一堆可選參數與組裝步驟,怎麼建得乾淨?ProblemMedium設計情境

需求:要建一個 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

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading

🔗 Dive Deeper