Builder
ค่อย ๆ กำหนดคุณลักษณะของ object ทีละขั้นด้วยหลาย method
เมื่อ object หนึ่งมีคุณลักษณะให้กำหนดจำนวนมาก และบางส่วนเป็น optional เราจะเจอปัญหาที่เรียกว่า “telescoping constructor” — คือต้องเขียน constructor หลายตัวซ้อนกันเพื่อรองรับทุก combination ของ parameter ที่จำเป็นกับที่ไม่จำเป็น เช่น Report(title), Report(title, author), Report(title, author, sections) ไล่ไปเรื่อย ๆ จนอ่านยากและแก้ยาก ทางเลือกที่แย่ไม่แพ้กันคือยัดทุกอย่างลงใน constructor เดียวที่รับ parameter ยาวเป็นสิบตัว หรือส่ง parameter object ที่ซับซ้อนเข้าไปแทน
Builder แก้ปัญหานี้ด้วยการ แยกกระบวนการสร้าง object ออกจากตัว representation ของมัน แทนที่จะมี method เดียวที่สร้างและคืนค่า object ทันทีแบบ Factory Method, Builder เปิด หลาย method ที่ค่อย ๆ นิยามคุณลักษณะของ object ทีละขั้น แล้วปิดท้ายด้วย method Build() ที่คืน object ที่ประกอบเสร็จสมบูรณ์ ระหว่างทางเราสามารถข้ามขั้นที่เป็น optional ไปได้ และ construction logic ที่ซับซ้อน (เช่น การตรวจ invariant หรือคำนวณค่าที่ขึ้นกับกัน) ก็ถูกซ่อนไว้ในตัว builder เอง ไม่รั่วไหลออกไปให้ client ต้องรู้
แนวคิดตาม Gang of Four คือมี Director ที่รู้ลำดับขั้นตอนการสร้าง คอยเรียก Builder interface ซึ่ง ConcreteBuilder แต่ละตัวจะประกอบ Product ออกมาต่างกันได้ แม้ construction process จะเหมือนกัน — เช่น builder เดียวกันอาจสร้างรายงานแบบ HTML หรือ PDF ก็ได้ ใน code จริงยุคปัจจุบัน (โดยเฉพาะสาย .NET/Java) มักตัด Director ออกแล้วให้ client เรียก builder แบบ fluent เอง เพราะเรียบง่ายกว่าและยังคงประโยชน์หลักไว้ครบ ตัวอย่างที่คุ้นเคยที่สุดคือ StringBuilder ใน .NET และ Java
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Director {
-IBuilder builder
+Construct()
}
class IBuilder {
<<interface>>
+BuildTitle(title)
+AddSection(section)
+Build() Report
}
class ReportBuilder {
-Report report
+BuildTitle(title)
+AddSection(section)
+Build() Report
}
class Report {
+Title
+Sections
}
Director o-- IBuilder
IBuilder <|.. ReportBuilder
ReportBuilder ..> Report
- Builder (
IBuilder) — นิยาม interface กลางของขั้นตอนการสร้าง ให้ ConcreteBuilder แต่ละตัวไป implement ต่างกันได้ - ConcreteBuilder (
ReportBuilder) — เก็บ state ระหว่างการประกอบ, implement แต่ละขั้น, และคืนผลลัพธ์สุดท้ายผ่านBuild() - Product (
Report) — object ที่ถูกประกอบขึ้น ไม่จำเป็นต้องรู้จัก builder เลย - Director — (มีหรือไม่มีก็ได้) ห่อหุ้มลำดับขั้นที่ใช้ซ้ำได้ เช่น “สร้างรายงานสรุปผู้บริหาร” ก็คือเรียก builder ตามลำดับคงที่ชุดหนึ่งเสมอ
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ในรูปแบบดั้งเดิมของ GoF, client สร้าง ConcreteBuilder แล้วส่งให้ Director ซึ่งจะขับเคลื่อนลำดับการเรียก method ของ builder ตามที่มันรู้ Director ไม่รู้จัก product โดยตรง มันรู้แค่ “ต้องเรียก builder ทำอะไรก่อนหลัง” ส่วน builder เป็นคนเดียวที่รู้รายละเอียดว่าจะประกอบ product ยังไง
ใน code C#/Java สมัยใหม่ มักตัด Director ออกและใช้ fluent interface: แต่ละ method ของ builder คืน this (หรือ IBuilder type เดิม) ทำให้เรียกต่อกันเป็นลูกโซ่ได้อ่านลื่นเหมือนประโยคภาษาอังกฤษ — Martin Fowler เรียกสไตล์นี้ว่า fluent interface และชี้ว่ามันเหมือน internal DSL มากกว่าจะเป็นแค่ method chaining ธรรมดา จุดสำคัญคือ client เลือกเรียกเฉพาะขั้นที่ต้องการ ข้ามขั้นที่เป็น optional ได้ แล้วปิดท้ายด้วย Build() เพื่อรับ product ที่สมบูรณ์
sequenceDiagram
participant Client
participant Builder as ReportBuilder
participant Product as Report
Client->>Builder: new ReportBuilder()
Client->>Builder: Title(t)
Builder-->>Client: this
Client->>Builder: AddSection(s1)
Builder-->>Client: this
Client->>Builder: AddSection(s2)
Builder-->>Client: this
Client->>Builder: Build()
Builder->>Product: สร้าง Report ที่สมบูรณ์
Builder-->>Client: Report
จุดที่ควรระวังคือ builder ต้องรับประกันว่า Build() จะไม่คืน object ที่อยู่ในสถานะไม่ถูกต้อง (invalid state) — ถ้ามี field ที่บังคับ ควรตรวจใน Build() แล้วโยน exception ถ้ายังไม่ครบ ไม่ใช่ปล่อยให้ product ครึ่ง ๆ กลาง ๆ หลุดออกไป
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// Product — object ที่ค่อนข้างซับซ้อน มีทั้ง field บังคับและ optionalpublic class Report{ public string Title { get; init; } = ""; public string? Author { get; init; } public IReadOnlyList<string> Sections { get; init; } = Array.Empty<string>(); public bool IncludeSummary { get; init; }}
// Builder interface — นิยามขั้นตอนที่เปิดให้ client เรียกpublic interface IReportBuilder{ IReportBuilder Title(string title); IReportBuilder Author(string author); IReportBuilder AddSection(string section); IReportBuilder WithSummary(); Report Build();}
// ConcreteBuilder — ประกอบทีละขั้นแบบ fluent แล้วตรวจ invariant ก่อนคืนผลpublic class ReportBuilder : IReportBuilder{ private string _title = ""; private string? _author; private readonly List<string> _sections = new(); private bool _includeSummary;
public IReportBuilder Title(string title) { _title = title; return this; }
public IReportBuilder Author(string author) { _author = author; return this; }
public IReportBuilder AddSection(string section) { _sections.Add(section); return this; }
public IReportBuilder WithSummary() { _includeSummary = true; return this; }
public Report Build() { // ตรวจ invariant ก่อนปล่อย product ออกไป กัน state ที่ไม่สมบูรณ์ if (string.IsNullOrWhiteSpace(_title)) throw new InvalidOperationException("Report ต้องมี Title เสมอ");
return new Report { Title = _title, Author = _author, Sections = _sections.AsReadOnly(), IncludeSummary = _includeSummary }; }}
// Director (ทางเลือก) — ห่อหุ้มลำดับขั้นที่ใช้ซ้ำได้เป็นสูตรสำเร็จpublic static class ReportDirector{ public static Report BuildExecutiveSummary(IReportBuilder builder, string title, string author) => builder.Title(title) .Author(author) .WithSummary() .AddSection("บทสรุปผู้บริหาร") .Build();}
// การใช้งานแบบ fluent ตรง ๆ โดยไม่ผ่าน Directorvar report = new ReportBuilder() .Title("ยอดขายรายเดือน") .AddSection("สรุป") .AddSection("รายละเอียด") // เพิ่มเฉพาะขั้นที่ต้องการ .Build();
// การใช้งานผ่าน Director เมื่อมีสูตรสร้างที่ใช้ซ้ำบ่อย ๆvar summary = ReportDirector.BuildExecutiveSummary(new ReportBuilder(), "Q3 Review", "สมชาย");เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- object ที่จะสร้างมีคุณลักษณะจำนวนมาก และหลายส่วนเป็น optional หรือมีค่า default
- ต้องการหลีกเลี่ยง telescoping constructor หรือ constructor ที่รับ parameter ยาวเกินไป (ดู Long Parameter List)
- construction logic มีความซับซ้อน เช่น ต้องตรวจ invariant, คำนวณค่าที่ขึ้นกับกัน หรือประกอบ object ทีละส่วนตามลำดับที่กำหนด
- ต้องการสร้าง representation ที่ต่างกันจาก construction process เดียวกัน (เช่น builder เดียวส่งออกได้ทั้ง HTML และ PDF report)
- ต้องการ object ที่เป็น immutable หลังสร้างเสร็จ แต่ระหว่างสร้างต้องปรับแต่งได้หลายขั้น
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- object เรียบง่าย มีไม่กี่ field ทุก field บังคับหมด — constructor ธรรมดาหรือ object initializer ก็เพียงพอ
- ทีมไม่ต้องการดูแล class เพิ่มอีกชุด (interface + concrete builder) สำหรับ object ที่ไม่ค่อยเปลี่ยนแปลงรูปแบบ
- ใช้ record type หรือ named/optional parameters ของภาษาสมัยใหม่ (เช่น C# 12 primary constructor, required properties) แก้ปัญหาได้ตรงจุดกว่าอยู่แล้ว
- ต้องการ object ที่ต้องคงความ valid ตลอดเวลาแม้ระหว่างสร้าง (Builder ยอมให้ state กลาง ๆ ไม่สมบูรณ์ได้ก่อนเรียก
Build())
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | สร้าง object ทีละขั้น ข้ามขั้นได้ตามต้องการ (Single Responsibility ของ construction logic) |
| ข้อดี | ใช้ construction process เดียวกันสร้าง representation ต่างกันได้ (ผ่าน ConcreteBuilder หลายตัว) |
| ข้อดี | ซ่อน state กลางที่ยังไม่สมบูรณ์จาก client จนกว่าจะเรียก Build() |
| ข้อดี | fluent interface อ่านง่าย ใกล้เคียงภาษาธรรมชาติ |
| ข้อเสีย | เพิ่มจำนวน class/code สำหรับ object ที่อาจไม่ซับซ้อนพอจะคุ้มค่า |
| ข้อเสีย | ถ้าลืมเรียก Build() หรือลืมกำหนด field บังคับ อาจได้ product ที่ไม่สมบูรณ์ ต้องมี validation คอยกัน |
| ข้อเสีย | fluent method ที่คืน this มักละเมิด Command-Query Separation (แต่เป็น trade-off ที่ยอมรับได้ในบริบทนี้) |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Factory Method
- Abstract Factory
- Object Mother
- Long Parameter List
- Keep It Simple
- Single Responsibility Principle
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/design-patterns/builder-pattern
- Builder — Refactoring.Guru
- Builder pattern — Wikipedia
- FluentInterface — Martin Fowler
- Design Patterns — Gang of Four (Gamma, Helm, Johnson, Vlissides)