builder pattern

- Published on
- /8 mins read/
Trong thiết kế hướng đối tượng, việc khởi tạo một đối tượng phức tạp với hàng chục thuộc tính (trong đó có cả trường bắt buộc lẫn trường tùy chọn) luôn là một bài toán hóc búa.
Lịch sử kỹ nghệ phần mềm đã chứng kiến hai cách tiếp cận thất bại:
- Telescoping Constructor Anti-Pattern: Tạo ra hàng loạt constructor quá tải với số lượng tham số tăng dần. Khi một class có 8 trường, bạn sẽ có những constructor với 6-7 tham số cùng kiểu
Stringhoặcint. Lập trình viên rất dễ truyền nhầm thứ tự(timeoutMs, retryCount)mà trình biên dịch không hề cảnh báo! - JavaBeans Pattern (No-arg Constructor + Setters): Khởi tạo đối tượng rỗng rồi gọi hàng loạt
setXxx(). Cách này tạo ra một thảm họa còn lớn hơn: Đối tượng bị phơi bày ở trạng thái nửa vời (inconsistent/half-baked state) trong quá trình khởi tạo, phá vỡ hoàn toàn tính đóng gói và không thể tạo ra các đối tượng bất biến (Immutable Objects) an toàn trong môi trường đa luồng.
Builder Pattern trong cuốn Effective Java của Joshua Bloch đã giải quyết triệt để hai bài toán trên. Tuy nhiên, ở cấp độ Kiến trúc sư phần mềm, chúng ta cần nhiều hơn thế: Làm sao để bắt lỗi thiếu tham số bắt buộc ngay tại thời điểm Compile-time thay vì Runtime? Làm sao kết hợp Builder với Java Records?
# bản đồ so sánh các mô hình khởi tạo đối tượng
# nhược điểm chí tử của Builder cổ điển và lời giải "staged Builder"
Trong triển khai Builder thông thường (kể cả @Builder của Lombok), mọi phương thức thiết lập đều là tùy chọn. Nếu đối tượng PaymentOrder bắt buộc phải có orderId, amount, và currency, nhưng lập trình viên vô tình quên gọi .amount(...):
// Builder thông thường: Compile thành công nhưng CRASH tại Runtime!
PaymentOrder order = PaymentOrder.builder()
.orderId("ORD-001")
// Quên set amount!
.currency(Currency.getInstance("VND"))
.build(); // 🔴 Ném ra IllegalStateException tại Runtime!# staged Builder pattern (step Builder): an toàn compile-time tuyệt đối
Bằng cách sử dụng kỹ thuật Interface Chaining (Chuỗi giao diện), chúng ta có thể hướng dẫn trình biên dịch Java ép buộc lập trình viên phải truyền đầy đủ các trường bắt buộc theo đúng thứ tự logic trước khi phương thức .build() xuất hiện trên gợi ý code của IDE!
# triển khai staged Builder chuẩn enterprise
package com.company.payment.domain;
import java.math.BigDecimal;
import java.util.Currency;
import java.util.Objects;
import java.util.Optional;
public final class PaymentOrder {
// Immutable Fields
private final String orderId;
private final BigDecimal amount;
private final Currency currency;
private final String description;
private final String callbackUrl;
private PaymentOrder(Builder builder) {
this.orderId = builder.orderId;
this.amount = builder.amount;
this.currency = builder.currency;
this.description = builder.description;
this.callbackUrl = builder.callbackUrl;
}
// Các Bước Ép Buộc (Stages)
public interface OrderIdStep {
AmountStep orderId(String orderId);
}
public interface AmountStep {
CurrencyStep amount(BigDecimal amount);
}
public interface CurrencyStep {
BuildStep currency(Currency currency);
}
public interface BuildStep {
BuildStep description(String description);
BuildStep callbackUrl(String callbackUrl);
PaymentOrder build();
}
// Entry point duy nhất
public static OrderIdStep stagedBuilder() {
return new Builder();
}
// Builder Implementation ẩn bên trong
private static class Builder implements OrderIdStep, AmountStep, CurrencyStep, BuildStep {
private String orderId;
private BigDecimal amount;
private Currency currency;
private String description;
private String callbackUrl;
@Override
public AmountStep orderId(String orderId) {
this.orderId = Objects.requireNonNull(orderId, "orderId không được null");
return this;
}
@Override
public CurrencyStep amount(BigDecimal amount) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("amount phải lớn hơn 0");
}
this.amount = amount;
return this;
}
@Override
public BuildStep currency(Currency currency) {
this.currency = Objects.requireNonNull(currency, "currency không được null");
return this;
}
@Override
public BuildStep description(String description) {
this.description = description;
return this;
}
@Override
public BuildStep callbackUrl(String callbackUrl) {
this.callbackUrl = callbackUrl;
return this;
}
@Override
public PaymentOrder build() {
return new PaymentOrder(this);
}
}
// Getters bảo vệ tính đóng gói
public String getOrderId() { return orderId; }
public BigDecimal getAmount() { return amount; }
public Currency getCurrency() { return currency; }
public Optional<String> getDescription() { return Optional.ofNullable(description); }
public Optional<String> getCallbackUrl() { return Optional.ofNullable(callbackUrl); }
}# trải nghiệm lập trình tuyệt vời
Nếu lập trình viên gõ:
PaymentOrder.stagedBuilder()
.orderId("TX-12345")
.amount(BigDecimal.valueOf(500000))
// Tại đây, IDE CHỈ CHO PHÉP gọi .currency(...)!
// Phương thức .build() hoàn toàn CHƯA TỒN TẠI!Lỗi thiếu tham số nghiệp vụ cốt lõi bị triệt tiêu 100% ngay từ lúc viết code, không bao giờ lọt tới bước Test hay Production!
# bản lĩnh xử lý bộ sưu tập bất biến (defensive copying)
Một lỗi rất phổ biến khi viết Builder là gán trực tiếp tham chiếu của List hoặc Map từ Builder sang đối tượng kết quả. Điều này cho phép bên ngoài thay đổi dữ liệu ngầm (mutate state):
// Anti-Pattern: Gán trực tiếp tham chiếu
public User build() {
this.roles = builder.roles; // 🔴 Lỗ hổng bảo mật: bên ngoài vẫn có thể gọi builder.roles.add("SUPERADMIN")!
}
// Hardened Pattern: Sao chép phòng thủ (Defensive Copy)
public User build() {
// Tạo bản sao bất biến không thể sửa đổi
this.roles = builder.roles == null ? Set.of() : Set.copyOf(builder.roles);
}# cuộc đối đầu: Lombok @Builder vs Java Records (Java 17+)
Từ Java 16/17, Java Records ra đời mang lại cú pháp siêu tinh gọn cho dữ liệu bất biến. Tuy nhiên, giữa Records và Builder có mối quan hệ bổ trợ lẫn nhau chứ không triệt tiêu nhau:
# bẫy ngầm khi dùng Lombok @Builder bạn phải biết
- Bẫy Default Value (
@Builder.Default): Nếu bạn khai báo giá trị mặc định:@Builder public class HttpClientConfig { @Builder.Default private int timeoutMs = 5000; // BẮT BUỘC PHẢI CÓ @Builder.Default, nếu không Lombok sẽ set về 0! } - Bẫy Jackson Deserialization: Khi deserialize JSON bằng Jackson sang một class có
@Builder, bạn bắt buộc phải thêm annotation@Jacksonized(hoặc@JsonDeserialize(builder = ...)), nếu không Jackson sẽ báo lỗi không tìm thấy no-arg constructor!
# ma trận đánh giá tổng hợp
| Tiêu chí | Telescoping Constructor | JavaBeans (Setters) | Effective Java Builder | Staged Builder | Java Record |
|---|---|---|---|---|---|
| Tính bất biến (Immutability) | 🟢 Có | 🔴 Không | 🟢 Hoàn toàn | 🟢 Hoàn toàn | 🟢 Mặc định 100% |
| Tính an toàn Compile-time | 🟡 Kém (dễ nhầm kiểu) | 🔴 Rất kém | 🟡 Runtime check | 🟢 Tuyệt đối 100% | 🟡 Theo vị trí constructor |
| Độ trong sáng của mã (Readability) | 🔴 Rất khó đọc | 🟢 Tốt | 🟢 Rất cao (Fluent) | 🟢 Rất cao | 🟢 Cao |
| Khả năng kiểm soát trạng thái | Tốt | 🔴 Đối tượng nửa vời | Tốt | Tối ưu nhất | Tốt |
| Chi phí viết mã (Boilerplate) | Trung bình | Thấp | Cao (hoặc dùng Lombok) | Khá cao | Cực kỳ thấp |
# tổng kết
Builder Pattern không chỉ dừng lại ở việc làm cho code "dễ nhìn" hơn với chuỗi phương thức (method chaining). Trong kiến trúc phần mềm chuyên nghiệp:
- Builder là công cụ bảo vệ tính bất biến: Ngăn chặn việc phát sinh các trạng thái lỗi trong môi trường tính toán đa luồng phân tán.
- Staged Builder là đỉnh cao của Type-Driven Development: Tận dụng hệ thống kiểu (Type System) của Java để biến các quy tắc nghiệp vụ bắt buộc thành các ràng buộc biên dịch, giúp mã nguồn trở nên tự sửa lỗi và loại bỏ hoàn toàn các lỗi runtime ngớ ngẩn.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- # bản đồ so sánh các mô hình khởi tạo đối tượng
- # nhược điểm chí tử của Builder cổ điển và lời giải "staged Builder"
- # staged Builder pattern (step Builder): an toàn compile-time tuyệt đối
- # triển khai staged Builder chuẩn enterprise
- # trải nghiệm lập trình tuyệt vời
- # bản lĩnh xử lý bộ sưu tập bất biến (defensive copying)
- # cuộc đối đầu: Lombok @Builder vs Java Records (Java 17+)
- # bẫy ngầm khi dùng Lombok @Builder bạn phải biết
- # ma trận đánh giá tổng hợp
- # tổng kết