Dòng new EmailSender() nằm sâu trong một class nghiệp vụ trông vô hại, cho tới ngày khách hàng yêu cầu gửi thêm SMS, rồi thêm Zalo, và bộ test của bạn không thể chạy nếu không có máy chủ SMTP. Factory Method và Abstract Factory là hai mẫu thiết kế tạo đối tượng (creational pattern) sinh ra để xử lý đúng tình huống này: tách việc "tạo ra cái gì" khỏi logic "dùng nó để làm gì". Bài này cài đặt cả hai trên một hệ thống thông báo đơn hàng và thanh toán, chạy thật, biên dịch với --release 17 (API và ngôn ngữ của Java 17 LTS), dán output thật, so sánh với static factory method trong JDK và nói rõ khi nào các mẫu này chỉ làm code rườm rà.
Bài viết tiếp nối Singleton pattern trong Java: lazy, holder, enum và Java Memory Model trong cùng series, và dùng lại tư duy đảo ngược phụ thuộc từ Nguyên lý SOLID trong Java qua ví dụ thực tế.
Vấn đề: new nằm lẫn trong logic nghiệp vụ
Hãy hình dung một class gửi thông báo "đơn đã giao" cho khách. Phiên bản ngây thơ tự tạo đối tượng gửi thư ngay trong method:
class OrderNotifier {
void orderShipped(String customer, String orderId) {
EmailSender s = new EmailSender(); // gắn chặt vào một lớp cụ thể
s.send(customer, "Don " + orderId + " da giao");
}
}Có ba hệ quả. Thứ nhất, muốn thêm kênh SMS phải sửa chính class này, vi phạm nguyên lý Open–Closed. Thứ hai, class nghiệp vụ phụ thuộc vào lớp cụ thể, trái với Dependency Inversion. Thứ ba, unit test không thể thay EmailSender bằng đối tượng giả. Factory Method giải quyết cả ba bằng một thay đổi nhỏ: dời câu lệnh new vào một method riêng mà lớp con có thể ghi đè.
Factory Method: cấu trúc và cài đặt
Theo mô tả của Refactoring.Guru, Factory Method định nghĩa một method tạo đối tượng trong lớp cha, để lớp con quyết định kiểu cụ thể được tạo. Mẫu này có bốn vai trò: Product là interface chung của mọi sản phẩm, Concrete Product là các cài đặt, Creator khai báo factory method trả về kiểu Product (đồng thời chứa logic nghiệp vụ dùng sản phẩm), và Concrete Creator ghi đè factory method để trả về sản phẩm cụ thể. Điểm hay bị hiểu sai: nhiệm vụ chính của Creator không phải là tạo đối tượng, mà là logic nghiệp vụ dựa trên Product; factory method chỉ là cái "móc" để lớp con cắm sản phẩm vào.
Áp vào ví dụ, Sender là Product, OrderNotifier là Creator với factory method createSender():
interface Sender { void send(String to, String text); }
static class EmailSender implements Sender {
public void send(String to, String text) { System.out.println("[EMAIL] " + to + " <- " + text); }
}
static class SmsSender implements Sender {
public void send(String to, String text) {
String cut = text.length() > 40 ? text.substring(0, 37) + "..." : text;
System.out.println("[SMS] " + to + " <- " + cut);
}
}
static abstract class OrderNotifier {
protected abstract Sender createSender(); // factory method
void orderShipped(String customer, String orderId) {
Sender s = createSender();
s.send(customer, "Don " + orderId + " da duoc giao cho don vi van chuyen");
}
}
static class EmailOrderNotifier extends OrderNotifier {
protected Sender createSender() { return new EmailSender(); }
}
static class SmsOrderNotifier extends OrderNotifier {
protected Sender createSender() { return new SmsSender(); }
}Thêm một Concrete Creator cho môi trường test, ghi lại tin nhắn thay vì gửi thật, không cần sửa dòng nào của OrderNotifier:
static class RecordingSender implements Sender {
final List<String> sent = new ArrayList<>();
public void send(String to, String text) { sent.add(to + "|" + text); }
}
static class TestNotifier extends OrderNotifier {
final RecordingSender rec = new RecordingSender();
protected Sender createSender() { return rec; }
}=== java -cp out FactoryMethodDemo
[EMAIL] an@example.com <- Don DH-2001 da duoc giao cho don vi van chuyen
[SMS] 0900000000 <- Don DH-2001 da duoc giao cho don vi v...
[TEST] da ghi 1 tin: test@example.com|Don DH-9 da duoc giao cho don vi van chuyenDòng thứ ba cho thấy lợi ích trực tiếp với test: nghiệp vụ gửi thông báo được kiểm tra mà không cần mạng. Lưu ý rằng Factory Method dựa trên kế thừa: mỗi biến thể là một lớp con của Creator. Khi số biến thể tăng, số lớp con tăng theo, đó là nhược điểm chính mà Refactoring.Guru cũng nêu.
Abstract Factory: tạo cả một họ đối tượng đi cùng nhau
Factory Method tạo một loại sản phẩm. Abstract Factory tạo một họ sản phẩm có liên quan và đảm bảo chúng luôn tương thích. Ví dụ thực tế: tích hợp cổng thanh toán có hai môi trường, sandbox và live. Mỗi môi trường cần một client gọi API và một bộ xác minh chữ ký webhook, và trộn client sandbox với verifier live sẽ dẫn tới lỗi khó tìm. Abstract Factory buộc hai thứ này đi cùng nhau:
interface PaymentClient { String charge(String orderId, long amount); }
interface WebhookVerifier { boolean verify(String payload, String signature); }
interface PaymentKit { // Abstract Factory
PaymentClient client();
WebhookVerifier verifier();
}
static class SandboxKit implements PaymentKit {
public PaymentClient client() { return new SandboxClient(); }
public WebhookVerifier verifier() { return new SandboxVerifier(); }
}
static class LiveKit implements PaymentKit {
public PaymentClient client() { return new LiveClient(); }
public WebhookVerifier verifier() { return new LiveVerifier(); }
}
static PaymentKit kitFor(String env) {
return switch (env) {
case "live" -> new LiveKit();
case "sandbox" -> new SandboxKit();
default -> throw new IllegalArgumentException("Moi truong khong ho tro: " + env);
};
}
static void handle(PaymentKit kit, String payload, String sig) {
System.out.println(kit.client().charge("DH-3001", 450_000)
+ " | webhook hop le = " + kit.verifier().verify(payload, sig));
}Ví dụ này không gọi mạng thật: các client là bản giả lập để tập trung vào cấu trúc. Hàm handle chỉ biết PaymentKit, không biết họ nào đang được dùng:
=== java -cp out AbstractFactoryDemo
sandbox: SANDBOX-OK DH-3001 450000 | webhook hop le = true
live: LIVE-OK DH-3001 450000 | webhook hop le = false
LOI: Moi truong khong ho tro: staging
Cùng chữ ký giả sig-gia, sandbox chấp nhận còn live từ chối: đúng hành vi mong muốn, và việc chuyển môi trường chỉ là đổi một tham số ở kitFor. Tên môi trường lạ như staging bị từ chối ngay, thay vì âm thầm rơi về một giá trị mặc định nguy hiểm.
Factory Method hay Abstract Factory
| Tiêu chí | Factory Method | Abstract Factory |
|---|---|---|
| Tạo gì | Một sản phẩm | Một họ sản phẩm liên quan |
| Cơ chế | Kế thừa: lớp con ghi đè method | Thành phần: truyền đối tượng factory vào |
| Thêm biến thể mới | Thêm một lớp con Creator | Thêm một factory cụ thể, đủ cả họ |
| Thêm loại sản phẩm mới vào họ | Không áp dụng | Phải sửa interface factory và mọi cài đặt (đắt) |
| Độ phức tạp | Thấp | Cao hơn |
Refactoring.Guru ghi nhận một con đường tiến hoá thông thường: bắt đầu bằng Factory Method (đơn giản, mở rộng qua lớp con), và chuyển sang Abstract Factory khi bắt đầu cần nhiều sản phẩm liên quan phải nhất quán. Bản thân Abstract Factory thường được xây từ một nhóm Factory Method. Ở ví dụ trên, method client() và verifier() chính là hai factory method nằm chung một interface.
Static factory method: biến thể đơn giản và phổ biến nhất trong JDK
Nhiều "factory" trong thực tế không có hệ thống lớp con nào cả mà chỉ là một method static trả về interface. Đây là mục đầu tiên trong Effective Java của Joshua Bloch: cân nhắc dùng static factory method thay cho constructor, vì chúng có tên gọi rõ nghĩa, có thể trả về bộ nhớ đệm hoặc kiểu con, và không bắt buộc tạo đối tượng mới. Cách này thường kết hợp với một registry để thêm biến thể bằng một dòng đăng ký thay vì sửa switch:
interface Shipper { String quote(int kg); }
record Standard() implements Shipper { public String quote(int kg) { return "Tieu chuan " + (20_000 + 5_000L * kg); } }
record Express() implements Shipper { public String quote(int kg) { return "Hoa toc " + (45_000 + 9_000L * kg); } }
static final Map<String, Supplier<Shipper>> REGISTRY = Map.of(
"standard", Standard::new,
"express", Express::new);
static Shipper of(String code) {
Supplier<Shipper> s = REGISTRY.get(code);
if (s == null) throw new IllegalArgumentException("Khong co don vi: " + code);
return s.get();
}=== java -cp out StaticFactoryDemo
Tieu chuan 35000
Hoa toc 72000
LOI: Khong co don vi: drone
Chính JDK dùng cách này khắp nơi. Tôi chạy thử một số factory phổ biến để xem kiểu thật được trả về:
=== java -cp out JdkFactories
Calendar.getInstance() -> java.util.GregorianCalendar
NumberFormat vi-VN -> java.text.DecimalFormat : 1.234.567,89
List.of(1) -> java.util.ImmutableCollections$List12
List.of(1,2,3) -> java.util.ImmutableCollections$ListN
Integer.valueOf(127) same -> true
Integer.valueOf(1000) same -> false
Charset.forName("UTF-8") -> sun.nio.cs.UTF_8
DocumentBuilderFactory -> com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl
List.of(...).add -> UnsupportedOperationExceptionNhững dòng này minh hoạ đúng các lý do dùng factory. Calendar là class trừu tượng, tài liệu Calendar mô tả getInstance là factory trả về lớp con phù hợp với locale và múi giờ, ở đây là GregorianCalendar. List.of chọn lớp cài đặt khác nhau theo số phần tử (List12 cho 1 đến 2 phần tử, ListN cho nhiều hơn), điều mà constructor không làm được. Integer.valueOf tái sử dụng đối tượng trong khoảng -128 đến 127 nên hai lần gọi với 127 trả về cùng một đối tượng, còn 1000 thì không, và đó là lý do so sánh Integer bằng == là một lỗi kinh điển.
Với XML, DocumentBuilderFactory.newInstance() là factory theo tinh thần Abstract Factory của JAXP (về mặt học thuật gần với factory tìm cài đặt qua service provider hơn là một họ sản phẩm): trả về bộ cài đặt XML mà bạn có thể thay đổi bằng cấu hình mà không sửa mã.
Factory trong Spring
Trong Spring, bản thân container là một factory khổng lồ: BeanFactory tạo và quản lý bean theo định nghĩa. Tài liệu Spring còn hỗ trợ khai báo bean tạo bằng static factory method hoặc instance factory method, và đưa ra interface FactoryBean cho những đối tượng có logic tạo phức tạp.
Hãy chú ý cách tài liệu Spring 5.3 phân biệt hai khái niệm: "factory bean" là bean tạo ra đối tượng khác bằng factory method, còn FactoryBean (viết hoa) là một interface đặc thù của Spring. Khi dùng Spring, nhu cầu tự viết Factory Method giảm đi nhiều vì container làm việc "chọn cài đặt nào" qua cấu hình, profile hoặc @ConditionalOnProperty; bạn chỉ còn tự viết factory khi kiểu cần tạo phụ thuộc dữ liệu lúc chạy, như mã đơn vị vận chuyển trong ví dụ trên.
Khi nào không nên dùng Factory Method
- Chỉ có một cài đặt và không có nhu cầu test thay thế: một class factory chỉ để bọc
newlà chi phí không đổi lấy lợi ích. Gọi constructor trực tiếp rõ ràng hơn. - Đã có DI container: để container tiêm cài đặt phù hợp qua constructor thay vì tự viết cây Creator.
- Số biến thể nhiều và độc lập: cây lớp con bùng nổ. Hãy chuyển sang registry hoặc truyền
Suppliernhư ở ví dụ trên, thay vì mỗi biến thể một lớp con. - Họ sản phẩm hay thay đổi cấu trúc: Abstract Factory cứng ở chỗ thêm loại sản phẩm mới buộc sửa mọi factory cụ thể. Chỉ dùng khi họ sản phẩm ổn định còn biến thể mới (môi trường, nền tảng) hay xuất hiện.
Lỗi thường gặp
- Gọi
newbên trong chính Creator ngoài factory method: phá vỡ mục đích tách rời. Hãy tìm mọi chỗnewcủa sản phẩm khi review. - Factory trả về
nullcho tham số lạ: lỗi lan ra xa nơi gây ra. NémIllegalArgumentExceptionvới thông báo rõ như ví dụkitFor("staging"). - Gọi factory method từ constructor của lớp cha: lớp con chưa được khởi tạo xong, các trường của nó vẫn là
nullhoặc 0. Tránh gọi method có thể bị ghi đè trong constructor. - Factory vừa tạo vừa chứa logic nghiệp vụ: trộn hai trách nhiệm, vi phạm SRP. Factory nên chỉ quyết định tạo gì.
- Nhầm với Simple Factory (một class có
switchtrả sản phẩm): hữu ích nhưng không phải Factory Method của GoF vì không dùng kế thừa để chọn kiểu. Đặt đúng tên giúp đồng đội hiểu ý định thiết kế. - Trộn sản phẩm của hai họ khi tự ghép tay mà không qua Abstract Factory: kiểm thử phải có ca bắt lỗi này.
Checklist và tóm tắt
- Logic nghiệp vụ có đang gọi
newcho một lớp có nhiều biến thể hoặc cần thay thế khi test? - Một sản phẩm: dùng Factory Method; một họ sản phẩm phải nhất quán: dùng Abstract Factory.
- Biến thể chọn theo dữ liệu lúc chạy: dùng static factory method hoặc registry
Map<String, Supplier<T>>. - Đã có Spring: ưu tiên để container chọn cài đặt qua cấu hình trước khi tự viết factory.
- Mọi tham số lạ phải gây lỗi rõ ràng, không trả
null.
Mã nguồn đầy đủ (FactoryMethodDemo, AbstractFactoryDemo, StaticFactoryDemo, JdkFactories) biên dịch bằng javac --release 17 -d out *.java và chạy bằng java -cp out <Tên> với mức API Java 17 (bản LTS), máy 16 GB RAM chạy macOS. Tên lớp JDK nội bộ (như sun.nio.cs.UTF_8) có thể khác giữa các bản JDK, vì vậy đừng phụ thuộc vào chúng trong mã của bạn.
Bài tiếp theo
Đọc tiếp: Builder pattern: từ Effective Java tới Lombok và record.



