99 Ngày Java — Ngày 93: Builder và Prototype
Một constructor với tám tham số, trong đó năm cái cùng kiểu String, là lời mời viết nhầm thứ tự. Builder tạo đối tượng từng bước với tên rõ ràng cho từng giá trị. Prototype giải bài toán khác: tạo đối tượng mới bằng cách sao chép một đối tượng có sẵn. Trong Java, phần khó của Prototype là sao chép cho đúng.

1. Vấn đề của constructor nhiều tham số
Khi một lớp có nhiều trường tùy chọn, người ta hay viết chuỗi constructor chồng nhau hoặc dùng setter. Constructor chồng nhau khó đọc ở chỗ gọi vì không biết tham số thứ tư là gì. Setter thì để đối tượng tồn tại ở trạng thái dở dang và không thể bất biến. Builder giữ được cả hai điều tốt: chỗ gọi dễ đọc và đối tượng kết quả bất biến.
// what is true? what is 3?
var request = new HttpCall("https://api.example", "POST", body, 3, true, null);
// with a builder
var request = HttpCall.builder("https://api.example")
.method("POST")
.body(body)
.retries(3)
.followRedirects(true)
.build();2. Viết một builder
Builder là một lớp lồng static giữ các giá trị tạm. Tham số bắt buộc đi vào phương thức tạo builder, tham số tùy chọn có giá trị mặc định. Mỗi phương thức gán giá trị rồi trả về this để nối chuỗi lời gọi. build() là nơi kiểm tra các ràng buộc liên quan tới nhiều trường, rồi tạo đối tượng bất biến. Với record, builder gọi canonical constructor, nơi kiểm tra từng trường.
record HttpCall(String url, String method, String body, int retries) {
static Builder builder(String url) { return new Builder(url); }
static final class Builder {
private final String url;
private String method = "GET";
private String body;
private int retries;
private Builder(String url) { this.url = Objects.requireNonNull(url); }
Builder method(String method) { this.method = method; return this; }
Builder body(String body) { this.body = body; return this; }
Builder retries(int retries) { this.retries = retries; return this; }
HttpCall build() {
if (body != null && method.equals("GET"))
throw new IllegalStateException("GET must not have a body");
return new HttpCall(url, method, body, retries);
}
}
}3. Prototype: sao chép thay vì tạo mới
Prototype hữu ích khi đối tượng mẫu tốn công dựng hoặc khi bạn cần nhiều biến thể chỉ khác vài trường. Java có sẵn clone(), nhưng cơ chế này nhiều bẫy: Cloneable không khai báo phương thức nào, clone() mặc định chỉ sao chép nông và bỏ qua constructor. Cách được khuyên dùng là copy constructor hoặc static factory sao chép, nơi bạn quyết định rõ trường nào dùng chung, trường nào sao chép sâu.
final class ReportTemplate {
private final String title;
private final List<String> columns;
ReportTemplate(String title, List<String> columns) {
this.title = title;
this.columns = List.copyOf(columns); // defensive copy
}
ReportTemplate(ReportTemplate source) { // copy constructor
this(source.title, source.columns);
}
ReportTemplate withTitle(String newTitle) { // variant of the prototype
return new ReportTemplate(newTitle, columns);
}
}4. Nông, sâu và khi nào dùng
Sao chép nông tạo đối tượng mới nhưng các trường tham chiếu vẫn trỏ tới cùng đối tượng con; sửa danh sách ở bản sao là sửa luôn bản gốc. Sao chép sâu nhân bản cả đối tượng con. Nếu các thành phần đều bất biến, sao chép nông là đủ và an toàn, và đó là lý do nên thiết kế bất biến ngay từ đầu. Record với các phương thức with... chính là Prototype dạng gọn.
Dùng builder khi có từ bốn tham số trở lên hoặc nhiều tham số tùy chọn.
Hai ba tham số bắt buộc thì constructor hoặc static factory rõ hơn.
JDK dùng builder ở HttpRequest.newBuilder(), StringBuilder, Stream.builder().
Tránh implements Cloneable trong code mới; mảng là ngoại lệ, clone() của mảng dùng tốt.
Builder làm việc tạo đối tượng dễ đọc, Prototype làm việc sao chép có chủ đích. Ngày 94 chuyển sang mẫu hành vi: Strategy và Observer.
Tài liệu đối chiếu
https://refactoring.guru/design-patterns/builder
https://refactoring.guru/design-patterns/prototype
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


