Builder
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Builder แยกการสร้าง object ที่ซับซ้อนออกจากการแทนค่าของตัวเอง ให้คุณประกอบ object ผ่านลำดับขั้นตอนที่ชัดเจน และผลิต product สำเร็จรูปออกมาเฉพาะตอนจบเท่านั้น
object บางตัวมีชิ้นส่วนเสริมจำนวนมาก HTTP request มี method, URL, header เสริม, body เสริม และ timeout การเข้ารหัสทุกการผสมผสานเป็น constructor นำไปสู่ทั้งรายการ overload ที่ยืดยาว หรือ constructor ยักษ์ตัวเดียวที่เต็มไปด้วย null และ boolean ที่คุณอ่านความหมายไม่ออก ณ จุดที่เรียกใช้ new Request("GET", url, null, null, 30, true, false) คือปริศนา ไม่ใช่ code
Builder แทนที่สิ่งนั้นด้วยขั้นตอนที่มีชื่อและต่อเชื่อมกันได้ คุณเริ่ม builder เรียกเฉพาะขั้นตอนที่คุณสนใจในลำดับใดก็ได้ และจบด้วย build() ตัวเดียวที่คืน product ที่ immutable และผ่านการตรวจสอบแล้ว จุดที่เรียกใช้อ่านได้เหมือนคำอธิบายสิ่งที่คุณต้องการ ตรรกะการสร้าง ทั้งค่าเริ่มต้น การตรวจสอบ และการประกอบ อยู่ใน builder แยกจากความรับผิดชอบของตัว product เอง
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class HttpRequest {
+method: string
+url: string
+headers: Map
+body: string
+summary() string
}
class RequestBuilder {
-method: string
-url: string
-headers: Map
-body: string
+setMethod(m) RequestBuilder
+addHeader(k, v) RequestBuilder
+setBody(b) RequestBuilder
+build() HttpRequest
}
RequestBuilder ..> HttpRequest : builds - Product (
HttpRequest) — object ที่ซับซ้อนซึ่งกำลังถูกประกอบขึ้น โดยทั่วไปจะ immutable เมื่อสร้างเสร็จ - Builder (
RequestBuilder) — เปิดเผยหนึ่งขั้นตอนต่อหนึ่งชิ้นส่วน แต่ละขั้นคืน builder กลับมาเพื่อให้เรียกต่อกันได้ บวกกับbuild()ตัวสุดท้ายที่ตรวจสอบและคืน product - Client — เรียกขั้นตอนที่ต้องการในลำดับใดก็ได้ แล้วจึง
build()
Director (class ที่ขับเคลื่อนลำดับการเรียก builder แบบตายตัว) เป็นส่วนเสริมที่มีก็ได้ ไม่มีก็ได้ สำหรับ fluent builder อย่างตัวนี้ client เองเล่นบทบาทนั้น
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”RequestBuilder แบบ fluent ที่ผลิต HttpRequest ที่ immutable
class HttpRequest { constructor( readonly method: string, readonly url: string, readonly headers: Record<string, string>, readonly body: string | null, ) {}
summary(): string { const headerCount = Object.keys(this.headers).length; return `${this.method} ${this.url} (${headerCount} headers, body: ${this.body !== null})`; }}
class RequestBuilder { private method = 'GET'; private headers: Record<string, string> = {}; private body: string | null = null;
constructor(private readonly url: string) {}
setMethod(method: string): this { this.method = method; return this; } addHeader(key: string, value: string): this { this.headers[key] = value; return this; } setBody(body: string): this { this.body = body; return this; } build(): HttpRequest { return new HttpRequest(this.method, this.url, this.headers, this.body); }}
const request = new RequestBuilder('https://api.example.com/items') .setMethod('POST') .addHeader('Content-Type', 'application/json') .setBody('{"name":"book"}') .build();
console.log(request.summary());from dataclasses import dataclass, field
@dataclass(frozen=True)class HttpRequest: method: str url: str headers: dict[str, str] body: str | None
def summary(self) -> str: return ( f"{self.method} {self.url} " f"({len(self.headers)} headers, body: {self.body is not None})" )
class RequestBuilder: def __init__(self, url: str) -> None: self._url = url self._method = "GET" self._headers: dict[str, str] = {} self._body: str | None = None
def set_method(self, method: str) -> "RequestBuilder": self._method = method return self
def add_header(self, key: str, value: str) -> "RequestBuilder": self._headers[key] = value return self
def set_body(self, body: str) -> "RequestBuilder": self._body = body return self
def build(self) -> HttpRequest: return HttpRequest(self._method, self._url, dict(self._headers), self._body)
request = ( RequestBuilder("https://api.example.com/items") .set_method("POST") .add_header("Content-Type", "application/json") .set_body('{"name":"book"}') .build())
print(request.summary())package main
import "fmt"
type HttpRequest struct { Method string URL string Headers map[string]string Body *string}
func (r HttpRequest) Summary() string { return fmt.Sprintf("%s %s (%d headers, body: %t)", r.Method, r.URL, len(r.Headers), r.Body != nil)}
type RequestBuilder struct { method string url string headers map[string]string body *string}
func NewRequestBuilder(url string) *RequestBuilder { return &RequestBuilder{method: "GET", url: url, headers: map[string]string{}}}
func (b *RequestBuilder) SetMethod(m string) *RequestBuilder { b.method = m return b}
func (b *RequestBuilder) AddHeader(k, v string) *RequestBuilder { b.headers[k] = v return b}
func (b *RequestBuilder) SetBody(body string) *RequestBuilder { b.body = &body return b}
func (b *RequestBuilder) Build() HttpRequest { return HttpRequest{Method: b.method, URL: b.url, Headers: b.headers, Body: b.body}}
func main() { request := NewRequestBuilder("https://api.example.com/items"). SetMethod("POST"). AddHeader("Content-Type", "application/json"). SetBody(`{"name":"book"}`). Build() fmt.Println(request.Summary())}use std::collections::HashMap;
struct HttpRequest { method: String, url: String, headers: HashMap<String, String>, body: Option<String>,}
impl HttpRequest { fn summary(&self) -> String { format!( "{} {} ({} headers, body: {})", self.method, self.url, self.headers.len(), self.body.is_some() ) }}
struct RequestBuilder { method: String, url: String, headers: HashMap<String, String>, body: Option<String>,}
impl RequestBuilder { fn new(url: &str) -> Self { RequestBuilder { method: "GET".to_string(), url: url.to_string(), headers: HashMap::new(), body: None, } }
fn method(mut self, method: &str) -> Self { self.method = method.to_string(); self }
fn header(mut self, key: &str, value: &str) -> Self { self.headers.insert(key.to_string(), value.to_string()); self }
fn body(mut self, body: &str) -> Self { self.body = Some(body.to_string()); self }
fn build(self) -> HttpRequest { HttpRequest { method: self.method, url: self.url, headers: self.headers, body: self.body, } }}
fn main() { let request = RequestBuilder::new("https://api.example.com/items") .method("POST") .header("Content-Type", "application/json") .body(r#"{"name":"book"}"#) .build(); println!("{}", request.summary());}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: จุดที่เรียกใช้อ่านได้ชัดเจน แต่ละขั้นตอนมีชื่อ ความหมายของทุกค่าจึงเห็นได้ชัด
- ข้อดี: จัดการชิ้นส่วนเสริมจำนวนมากได้โดยไม่เกิดการระเบิดเชิงผสมของ constructor
- ข้อดี: การตรวจสอบและค่าเริ่มต้นอยู่ในที่เดียว และ product สำเร็จรูปสามารถเป็น immutable ได้
- ข้อเสีย: code มากกว่า constructor คุ้มก็ต่อเมื่อ object ซับซ้อนจริง ๆ
- ข้อเสีย: builder ที่สร้างไปครึ่งทางอยู่ในสถานะที่ไม่สมบูรณ์ ดังนั้น
build()ควรตรวจสอบ field ที่จำเป็น
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Abstract Factory คืน product ในการเรียกครั้งเดียว ส่วน Builder สร้างผ่านหลายขั้นตอน
- Prototype เป็นทางเลือกเมื่อมี object ที่ตั้งค่าไว้แล้วอยู่ และคุณต้องการสำเนา
| Builder | Factory Method | Prototype | |
|---|---|---|---|
| สร้างอะไร | object ที่มี config ซับซ้อน | product เดี่ยวจาก subclass | object ที่ clone จากต้นแบบ |
| ขั้นตอน | หลายขั้น เรียงตามลำดับ | ขั้นเดียว | ขั้นเดียว (clone) |
| เมื่อใช้ | มี optional field จำนวนมาก | ต้องการ defer การสร้าง | ต้องการ copy object ที่ตั้งค่าแล้ว |
| ตัวอย่างใน framework | StringBuilder, query builder | LoggerFactory, RouterFactory | Object.create(), clone() |