Chuyển tới nội dung chính

Builder Pattern (Mẫu Builder)

Định nghĩa (Definition)

Mẫu Builder tách biệt việc xây dựng (Construction) một đối tượng phức tạp khỏi biểu diễn (Representation) của nó, cho phép cùng một quá trình xây dựng tạo ra các biểu diễn khác nhau. Thay vì một constructor với quá nhiều tham số (Telescoping Constructor), bạn xây dựng đối tượng từng bước.

Ví dụ Coffee Shop

Một đơn hàng cà phê tùy chỉnh (Custom Coffee Order) có thể có nhiều phần tùy chọn: kích cỡ (Size), extra shots, loại sữa (Milk Type), syrup, topping, và hướng dẫn đặc biệt. Một constructor với tất cả tham số này sẽ rất khó đọc — Builder giải quyết vấn đề này.

Vấn đề: Telescoping Constructor (Constructor kéo dài)

// Đừng làm thế này
var coffee = new CustomCoffee(
"Latte", // name
CupSize.Large, // size
2, // extra shots
"Oat", // milk type
"Vanilla", // syrup
true, // whipped cream
false, // iced
"Extra hot" // special instructions
);
// Tham số nào là "Oat" nhỉ? Khó đọc, khó bảo trì.

Triển khai (Implementation)

public class CustomCoffee
{
public string Name { get; private set; } = "";
public CupSize Size { get; private set; } = CupSize.Medium;
public int ExtraShots { get; private set; }
public string MilkType { get; private set; } = "Whole";
public string? Syrup { get; private set; }
public bool WhippedCream { get; private set; }
public bool Iced { get; private set; }
public string? SpecialInstructions { get; private set; }
public List<string> Toppings { get; private set; } = new();

public override string ToString()
{
var parts = new List<string> { $"{Name} ({Size})" };
if (ExtraShots > 0) parts.Add($"+{ExtraShots} shots");
if (MilkType != "Whole") parts.Add($"{MilkType} milk");
if (Syrup is not null) parts.Add($"{Syrup} syrup");
if (WhippedCream) parts.Add("whipped cream");
if (Iced) parts.Add("iced");
if (Toppings.Count > 0) parts.Add($"topped with {string.Join(", ", Toppings)}");
if (SpecialInstructions is not null) parts.Add($"[{SpecialInstructions}]");
return string.Join(" | ", parts);
}

// Builder — nested class có truy cập vào private setter
public class Builder
{
private readonly CustomCoffee _coffee = new();

public Builder(string name)
{
_coffee.Name = name;
}

public Builder WithSize(CupSize size)
{
_coffee.Size = size;
return this;
}

public Builder WithExtraShots(int shots)
{
_coffee.ExtraShots = shots;
return this;
}

public Builder WithMilk(string milkType)
{
_coffee.MilkType = milkType;
return this;
}

public Builder WithSyrup(string syrup)
{
_coffee.Syrup = syrup;
return this;
}

public Builder WithWhippedCream()
{
_coffee.WhippedCream = true;
return this;
}

public Builder MakeIced()
{
_coffee.Iced = true;
return this;
}

public Builder WithTopping(string topping)
{
_coffee.Toppings.Add(topping);
return this;
}

public Builder WithSpecialInstructions(string instructions)
{
_coffee.SpecialInstructions = instructions;
return this;
}

public CustomCoffee Build()
{
// Validation (Xác thực)
if (string.IsNullOrWhiteSpace(_coffee.Name))
throw new InvalidOperationException("Coffee name is required.");

if (_coffee.Iced && _coffee.SpecialInstructions == "Extra hot")
throw new InvalidOperationException("Iced coffee cannot be extra hot.");

return _coffee;
}
}
}

Sử dụng — Fluent Builder

var coffee = new CustomCoffee.Builder("Latte")
.WithSize(CupSize.Large)
.WithExtraShots(2)
.WithMilk("Oat")
.WithSyrup("Vanilla")
.WithWhippedCream()
.WithTopping("Caramel Drizzle")
.WithTopping("Chocolate Shavings")
.WithSpecialInstructions("Extra hot")
.Build();

Console.WriteLine(coffee);
// Output: Latte (Large) | +2 shots | Oat milk | Vanilla syrup | whipped cream | topped with Caramel Drizzle, Chocolate Shavings | [Extra hot]

// Đơn hàng tối giản — chỉ tham số bắt buộc
var simple = new CustomCoffee.Builder("Espresso")
.WithSize(CupSize.Small)
.Build();
// Output: Espresso (Small)

Director — Công thức tái sử dụng (Reusable Recipes)

Director định nghĩa các chuỗi xây dựng tiêu chuẩn — giống như barista theo một công thức:

public class BaristaDirector
{
private readonly CustomCoffee.Builder _builder;

public BaristaDirector(CustomCoffee.Builder builder)
{
_builder = builder;
}

public CustomCoffee MakeSignatureMocha()
{
return _builder
.WithSize(CupSize.Large)
.WithExtraShots(1)
.WithMilk("Whole")
.WithSyrup("Chocolate")
.WithWhippedCream()
.WithTopping("Cocoa Powder")
.Build();
}

public CustomCoffee MakeIcedVanillaLatte()
{
return _builder
.WithSize(CupSize.Large)
.WithMilk("Almond")
.WithSyrup("Vanilla")
.MakeIced()
.Build();
}
}

// Usage
var director = new BaristaDirector(new CustomCoffee.Builder("Mocha"));
var signature = director.MakeSignatureMocha();

Builder dựa trên Record (C# hiện đại)

Sử dụng init property và with expression, bạn có thể đạt được pattern tương tự builder mà không cần Builder class riêng:

public record CustomCoffeeOrder(
string Name,
CupSize Size = CupSize.Medium,
int ExtraShots = 0,
string MilkType = "Whole",
string? Syrup = null,
bool WhippedCream = false,
bool Iced = false
);

// Usage — named arguments hoạt động như builder
var order = new CustomCoffeeOrder(
Name: "Latte",
Size: CupSize.Large,
MilkType: "Oat",
Syrup: "Vanilla",
WhippedCream: true
);

// Sao chép và sửa đổi bằng 'with' expression
var icedVersion = order with { Iced = true, MilkType = "Almond" };

StringBuilder — Builder tích hợp sẵn của .NET

System.Text.StringBuilder chính là một Builder pattern — bạn xây dựng string từng bước thay vì tạo nhiều string trung gian:

var receipt = new StringBuilder();
receipt.AppendLine("=== Coffee Receipt ===");
receipt.AppendLine($"Latte (Large) $4.00");
receipt.AppendLine($" + Oat milk $0.60");
receipt.AppendLine($" + Vanilla $0.50");
receipt.AppendLine($"-----------------------");
receipt.AppendLine($"Total: $5.10");
Console.WriteLine(receipt.ToString());

Khi nào sử dụng (When to Use)

  • Đối tượng có nhiều tham số tùy chọn hoặc bước cấu hình
  • Cần đối tượng bất biến (Immutable Objects) với nhiều property (kết hợp với init hoặc record)
  • Quá trình xây dựng (Construction Process) cần hỗ trợ nhiều biểu diễn khác nhau (cùng bước, kết quả khác)
  • Cần xác thực (Validation) trước khi đối tượng hoàn toàn được tạo

Khi nào KHÔNG sử dụng (When NOT to Use)

  • Đối tượng đơn giản (2-3 trường, tất cả bắt buộc) — dùng constructor
  • Chỉ có một cách xây dựng đối tượng — không cần kiểm soát từng bước
  • Builder không thêm validation hay logic nào so với constructor — đây là overhead không cần thiết

Điểm chính (Key Takeaways)

  • Builder tách biệt xây dựng (Construction) khỏi biểu diễn (Representation) — cùng các bước có thể tạo kết quả khác nhau
  • Fluent API (return this) làm builder dễ đọc và có thể xâu chuỗi (Chainable)
  • Director đóng gói các công thức phổ biến (Standard Recipes) — tái sử dụng cấu hình tiêu chuẩn
  • Trong C# hiện đại, record với named argument và with expression có thể thay thế builder cho trường hợp đơn giản
  • StringBuilder là một Builder thực tế mà bạn đã sử dụng

Câu hỏi phỏng vấn (Interview Questions)

Q: Khác biệt giữa Builder và Factory Method là gì? Factory Method tập trung vào class nào được khởi tạo. Builder tập trung vào cách xây dựng đối tượng phức tạp từng bước. Factory trả về sản phẩm trong một lần gọi; Builder trả về sau nhiều bước.

Q: Luôn cần Director không? Không. Director là tùy chọn — chỉ dùng khi có các công thức tiêu chuẩn (Standard Recipes) cần tái sử dụng. Nếu mỗi lần xây dựng là duy nhất, bỏ qua Director và dùng Builder trực tiếp.

Q: Builder liên quan đến Immutable Object như thế nào? Builder là cách tiêu chuẩn để xây dựng immutable object với nhiều trường. Object không có setter; Builder thu thập trạng thái (State) và tạo object cuối cùng trong Build().

  • Abstract Factory — tạo họ sản phẩm, Builder xây dựng một đối tượng phức tạp
  • Prototype — sao chép thay vì xây dựng
  • Structs & Recordsrecord type như một phương án thay thế builder hiện đại