Builder — รูปแบบการสร้างที่อนุญาตให้สร้างวัตถุที่ซับซ้อนทีละขั้นตอน แตกต่างจากคอนสตรักเตอร์ที่มีพารามิเตอร์หลายสิบตัว Builder ประกอบวัตถุผ่านลูกโซ่ของการเรียก แต่ละครั้งกำหนดค่าหนึ่งฟิลด์ รูปแบบนี้มีประโยชน์โดยเฉพาะสำหรับวัตถุที่มีพารามิเตอร์เลือกได้จำนวนมาก: การกำหนดค่าไคลเอ็นต์เครือข่าย การตั้งค่าฐานข้อมูล ตัวสร้างการแจ้งเตือนและการนำทาง เรียนรู้เพิ่มเติมที่ Refactoring Guru: Builder
ประเด็นสำคัญ
Builder — รูปแบบการสร้าง GoF ที่แยกการสร้างวัตถุที่ซับซ้อนออกจากการแสดงผล กระบวนการสร้างเดียวกันสามารถสร้างการแสดงผลที่แตกต่างกัน Builder มีประโยชน์เมื่อวัตถุมีพารามิเตอร์เลือกได้จำนวนมากและคอนสตรักเตอร์ที่มีสิบฟิลด์อ่านยากและยืดหยุ่นไม่ได้ รูปแบบนี้ยังแก้ปัญหา Telescoping Constructor ซึ่งเป็นปฏิรูปที่จำนวนการโอเวอร์โหลดของคอนสตรักเตอร์เพิ่มขึ้นแบบทวีคูณ
โครงสร้างของ Builder รวมถึงคลาส Builder คงที่ภายในที่มีฟิลด์ที่สะท้อนฟิลด์ของคลาสหลัก แต่ละ set-เมธอดส่งคืน Builder (this) สำหรับการโยงแบบลื่นไหล เมธอด build() สุดท้ายสร้างวัตถุเป้าหมายโดยส่งค่าฟิลด์ไปยังคอนสตรักเตอร์ส่วนตัว คลาสหลักมีคอนสตรักเตอร์ส่วนตัวที่รับ Builder ไคลเอ็นต์: Object.builder().setField1(val1).setField2(val2).build()
เมื่อใดควรใช้ Builder — วัตถุที่มี 5+ ฟิลด์โดยมีเพียง 2-3 ฟิลด์ที่จำเป็น วัตถุการกำหนดค่า (RequestConfig, DatabaseConfig) วัตถุที่มีตรรกะการตรวจสอบที่ซับซ้อนระหว่างการสร้าง วัตถุที่ต้องไม่เปลี่ยนแปล (immutable) หลังจากการสร้าง ใน Android Builder ถูกใช้อย่างแข็งขันใน SDK: AlertDialog.Builder, Retrofit.Builder, OkHttpClient.Builder, NotificationCompat.Builder
Builder ใน Kotlin มีสองแนวทาง: Builder แบบ Java คลาสสิก (ผ่านคลาสที่ซ้อนกัน) และ DSL builder แบบ Kotlin (ผ่านแลมบ์ดากับตัวรับ) Builder แบบ Java เหมาะสำหรับความเข้ากันได้กับ Android และเมื่อใช้กับโค้ด Java DSL builder เป็นวิธีธรรมชาติของ Kotlin: ฟังก์ชันรับแลมบ์ดาซึ่งภายใน this คือบริบท Builder ที่สามารถกำหนดฟิลด์ได้โดยตรง
// Builder แบบคลาสสิก
data class HttpConfig private constructor(
val baseUrl: String,
val timeout: Long = 30_000,
val retries: Int = 3,
val headers: Map<String, String> = emptyMap()
) {
class Builder {
private var baseUrl: String = ""
private var timeout: Long = 30_000
private var retries: Int = 3
private var headers: MutableMap<String, String> = mutableMapOf()
fun baseUrl(url: String) = apply { this.baseUrl = url }
fun timeout(ms: Long) = apply { this.timeout = ms }
fun retries(n: Int) = apply { this.retries = n }
fun header(key: String, value: String) = apply { headers[key] = value }
fun build(): HttpConfig {
require(baseUrl.isNotBlank()) { "baseUrl is required" }
return HttpConfig(baseUrl, timeout, retries, headers)
}
}
}
// การใช้งาน
val config = HttpConfig.Builder()
.baseUrl("https://api.example.com")
.timeout(15_000)
.header("Authorization", "Bearer token")
.build()
Kotlin DSL builder — ทางเลือกที่ไม่มีคลาสซ้อนกัน ฟังก์ชัน builder รับแลมบ์ดาในบริบทของวัตถุผู้สร้าง ซึ่งเป็นธรรมชาติสำหรับ Kotlin และไม่ต้องใช้ฟิลด์ write DSL builders ถูกใช้อย่างแข็งขันใน Ktor Client, kotlinx.serialization, Compose (Modifier) DSL builder ไม่เข้ากันได้กับ Java และไม่เหมาะสำหรับไลบรารีที่มี Java API
Builder ใน Swift — Swift ไม่มีรูปแบบ Builder ในตัว แต่อินเทอร์เฟซที่ลื่นไหลสามารถใช้งานได้ง่ายผ่านเมธอดที่ส่งคืน Self แต่ละเมธอดกำหนดค่าคุณสมบัติและส่งคืน self แตกต่างจาก Kotlin, Swift ไม่ต้องการคลาส Builder แยกต่างหาก — คุณสามารถส่งคืนวัตถุนั้นได้เองหากสามารถเปลี่ยนแปลงได้ระหว่างการประกอบ สำหรับวัตถุที่ไม่เปลี่ยนแปล จะใช้คลาส Builder ที่ซ้อนกันเช่นเดียวกับ Kotlin
struct NetworkRequest {
let url: String
let method: HTTPMethod
let headers: [String: String]
let body: Data?
let timeout: TimeInterval
final class Builder {
private var url: String = ""
private var method: HTTPMethod = .get
private var headers: [String: String] = [:]
private var body: Data? = nil
private var timeout: TimeInterval = 30
func withURL(_: String) -> Self { /* self */ }
func withMethod(_: HTTPMethod) -> Self { /* self */ }
func withHeader(key: String, value: String) -> Self { /* self */ }
func withBody(_: Data) -> Self { /* self */ }
func withTimeout(_: TimeInterval) -> Self { /* self */ }
func build() throws -> NetworkRequest {
guard !url.isEmpty else { throw BuilderError.missingURL }
return NetworkRequest(
url: url, method: method, headers: headers,
body: body, timeout: timeout
)
}
}
}
Result Builders — Swift 5.4 แนะนำ @resultBuilder — กลไกภาษาเพื่อการสร้างโครงสร้างแบบประกาศ SwiftUI, AttributedString, SceneBuilder ใช้ result builders นี่เป็นทางเลือกแทน Builder แบบคลาสสิก: แทนที่จะเป็นลูกโซ่ของ set-เมธอด result builder ใช้บล็อคโค้ดที่มีองค์ประกอบที่คอมไพเลอร์ประกอบเป็นอาร์เรย์หรือต้นไม้ @ViewBuilder ใน SwiftUI เป็นตัวอย่างที่รู้จักกันดีที่สุด: ภายใน body สามารถเขียน if, switch, ForEach และคอมไพเลอร์สร้าง View จากเงื่อนไข
Telescoping Constructor — ปฏิรูปที่คลาสมีคอนสตรักเตอร์ที่โอเวอร์โหลดหลายตัวพร้อมชุดพารามิเตอร์ที่แตกต่างกัน ตัวอย่างเช่น สามคอนสตรักเตอร์: HttpConfig(url), HttpConfig(url, timeout), HttpConfig(url, timeout, retries) เมื่อพารามิเตอร์เพิ่มขึ้น จำนวนคอนสตรักเตอร์เพิ่มขึ้นแบบทวีคูณ — สำหรับ n ฟิลด์เลือกได้ ต้องการ n! ชุดค่าผสม Builder แก้ปัญหานี้โดยอนุญาตให้ระบุเฉพาะฟิลด์ที่จำเป็น
| คุณลักษณะ | Telescoping Constructor | Builder | Kotlin named args |
|---|---|---|---|
| ปริมาณโค้ด | เพิ่มขึ้นแบบทวีคูณ | เพิ่มขึ้นเชิงเส้น | น้อยที่สุด |
| ความอ่านง่าย | ต่ำ (พารามิเตอร์ไหนคืออะไร?) | สูง (เมธอด + ชื่อ) | สูง (ชื่อ = ค่า) |
| ความไม่เปลี่ยนแปลง | ไม่เปลี่ยนแปล | ไม่เปลี่ยนแปล | ไม่เปลี่ยนแปล |
| ความเข้ากันได้กับ Java | เต็ม | เต็ม | ไม่มี (เฉพาะ Kotlin) |
| การตรวจสอบ | ในแต่ละคอนสตรักเตอร์ | ใน build() — ครั้งเดียว | ใน init() |
Kotlin named arguments + ค่าเริ่มต้น — ทางเลือกที่หรูหราสำหรับ Builder ในโปรเจกต์ Kotlin บริสุทธิ์ พารามิเตอร์ของคอนสตรักเตอร์มีค่าเริ่มต้น ไคลเอ็นต์ส่งเฉพาะที่จำเป็น: HttpConfig(baseUrl = url, timeout = 15_000) ข้อเสียคือไม่สามารถตรวจสอบฟิลด์ที่จำเป็นในเวลาคอมไพล์ Builder ให้ฟิลด์ที่จำเป็นผ่านคอนสตรักเตอร์ของ Builder (baseUrl จำเป็น) สำหรับไลบรารี Java Builder ยังคงเป็นมาตรฐานโดยพฤตินัย
Builder ใน Android SDK — หนึ่งในรูปแบบที่พบบ่อยที่สุดในไลบรารีมาตรฐาน AlertDialog.Builder: new AlertDialog.Builder(context).setTitle().setMessage().setPositiveButton().create() Retrofit.Builder: new Retrofit.Builder().baseUrl().addConverterFactory().build() OkHttpClient.Builder: new OkHttpClient.Builder().connectTimeout().addInterceptor().build() NotificationCompat.Builder: setContentTitle().setContentText().setSmallIcon().build()
เหตุใด Google จึงใช้ Builder — ความเข้ากันได้ย้อนหลัง การเพิ่มเมธอดใหม่ใน Builder ไม่ทำลายโค้ดที่มีอยู่ ถ้า Google ใช้คอนสตรักเตอร์ที่มี 20 พารามิเตอร์ แต่ละฟิลด์ใหม่จะต้องมีการโอเวอร์โหลดใหม่ Builder อนุญาตให้เพิ่ม set-เมธอดเป็นเวลาหลายปีโดยไม่มีการเปลี่ยนแปลงที่ทำลาย ตัวอย่างเช่น NotificationCompat.Builder เพิ่ม setBubbleMetadata() ใน Android 11 โดยไม่ส่งผลกระทบต่อโค้ดที่มีอยู่
Builder ในไลบรารี Kotlin — Ktor (HttpClientBuilder), Coil (ImageRequest.Builder), Room (Room.databaseBuilder(context, AppDatabase.class, "db").fallbackToDestructiveMigration().build()), Navigation (NavOptionsBuilder) ในโปรเจกต์ Kotlin Builder มักถูกรวมกับ DSL: Room.databaseBuilder(context, AppDatabase.class, "db").fallbackToDestructiveMigration().build() รูปแบบนี้ยังคงเกี่ยวข้องสำหรับ API สาธารณะที่ความเข้ากันได้ย้อนหลังและการทำงานร่วมกับ Java มีความสำคัญ
คำถามที่พบบ่อย
Builder มากเกินไปสำหรับวัตถุที่มี 1-3 ฟิลด์ — คอนสตรักเตอร์ปกติหรือ data class ชัดเจนกว่า นอกจากนี้ยังมากเกินไปในโปรเจกต์ Kotlin ที่ไม่มีการทำงานร่วมกับ Java ซึ่ง named arguments + ค่าเริ่มต้นแก้ปัญหาเดียวกันได้ง่ายกว่า Builder เหมาะสมสำหรับ 5+ ฟิลด์ การตรวจสอบที่ซับซ้อน หรือ Java API ที่ไม่มี named arguments
Builder สร้างวัตถุที่ซับซ้อนหนึ่งชิ้นทีละขั้นตอน (การกำหนดค่าฟิลด์) Factory สร้างวัตถุทั้งหมดตามประเภทหรือพารามิเตอร์ Builder ตอบคำถาม «ประกอบอย่างไร?» Factory ตอบ «สร้างอะไร?» Builder มักจะรวมกับ Factory: Factory เลือกประเภท Builder กำหนดค่าฟิลด์
ใน SwiftUI บทบาทของ Builder ดำเนินการโดย result builders (@ViewBuilder, @SceneBuilder) และตัวปรับแต่ง View (.font(), .padding()) ไม่จำเป็นต้องใช้ Builder แบบคลาสสิกเพราะ SwiftUI ใช้แนวทางการประกาศและตัวปรับแต่งที่ลื่นไหล สำหรับส่วนประกอบ UIKit Builder มีประโยชน์: UIAlertController, URLRequest, NSAttributedString
Builder โดยทั่วไปไม่ต้องการความปลอดภัยของเธรดเนื่องจากใช้ในเธรดเดียวเพื่อประกอบวัตถุ ถ้า Builder ถูกใช้ในสภาพแวดล้อมแบบหลายเธรด (กรณียาก) ให้ซิงโครไนซ์แต่ละ set-เมธอดและ build() ทางเลือก — Immutable Builder: แต่ละ set-เมธอดส่งคืนอินสแตนซ์ Builder ใหม่พร้อมฟิลด์ที่ถูกแก้ไข
Retrofit.Builder เป็น API ไลบรารีสาธารณะที่ต้องทำงานโดยไม่มีคอนเทนเนอร์ DI Builder ให้ความยืดหยุ่นในการกำหนดค่า (baseUrl ตัวแปลง ตัวสกัดกั้น ตัวปรับแต่งการเรียก) โดยไม่ต้องพึ่งพา Dagger หรือเฟรมเวิร์ก DI อื่น ๆ ภายในแอปพลิเคชัน DI สามารถสร้าง Retrofit ครั้งเดียวผ่าน Builder แต่ Builder เองยังคงเป็นส่วนหนึ่งของ API สาธารณะของ Retrofit
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม