factory pattern
Tung Bui Thanh (TungDaDev)10 mins read

Thử mở code dự án bạn đang làm xem: có class
PaymentFactoryhayNotificationFactorynào chứa một khốiswitch-casehay chuỗiif-else ifdài cả 200 dòng không? Mỗi lần thêm một cổng thanh toán mới (VNPay, MoMo, ZaloPay, Stripe) là lại có người nhảy vào sửa class Factory đó, và lại có nguy cơ làm hỏng mấy case cũ. Trớ trêu ở chỗ: Factory Pattern sinh ra để giúp code tuân thủ Open/Closed (mở để mở rộng, đóng để sửa đổi), nhưng viết kiểu này thì chính nó lại phá OCP nặng nhất!
Ở hệ thống enterprise lớn, việc tạo object không thể mãi dựa vào mấy cấu trúc rẽ nhánh viết cứng. Mình cần kiểu dynamic factory: tự phát hiện implementation (auto-discovery), tự đăng ký (self-registration), thêm bao nhiêu loại cũng được mà không phải sửa dòng code cũ nào.
Bài này đi từ Factory GoF cổ điển tới Dynamic Factory dựa trên Spring, rồi tới Functional Factory trong Java hiện đại.
# cái bẫy của Factory kinh điển (the ocp violation trap)
Đây là cách viết Factory mà gần như sách nhập môn hay tutorial nào cũng dạy:
// CÁI BẪY ANTI-PATTERN: Vi phạm nghiêm trọng Open/Closed Principle!
public class HardcodedPaymentFactory {
public PaymentGateway createGateway(PaymentType type) {
return switch (type) {
case VNPAY -> new VnPayGateway();
case MOMO -> new MoMoGateway();
case STRIPE -> new StripeGateway();
// Thêm cổng thanh toán mới -> BẮT BUỘC PHẢI SỬA CLASS NÀY!
default -> throw new UnsupportedOperationException("Không hỗ trợ");
};
}
}# sao lại gọi là thiết kế tồi?
- Phá OCP: Muốn mở rộng thì buộc phải sửa code đang chạy ổn.
- Dính chặt vào việc khởi tạo: Nếu
VnPayGatewaycần inject mấy dependency phức tạp (HttpClient,AuditService,SecretKeyProvider), class Factory sẽ thành một "bãi rác dependency".
Công bằng mà nói, switch trên enum cũng có một cái lợi: nếu bỏ nhánh default đi, thêm hằng số mới vào enum là compiler báo ngay chỗ nào chưa xử lý. Với vài loại ít khi đổi thì cách này vẫn ổn. Nó chỉ thành vấn đề khi danh sách cứ dài ra và mỗi nhánh kéo theo một đống dependency.
# cách làm trong Spring: dynamic Factory (map injection)
Với Spring Boot, mình bỏ hẳn được khối switch-case nhờ chuyện Spring IoC Container tự inject cả List hoặc Map bean.
# cách triển khai
# interface có kèm định danh
public interface PaymentGateway {
PaymentResult process(PaymentRequest request);
PaymentType getSupportedType(); // Trả về Enum hoặc String định danh
}# mỗi gateway một class riêng
@Component("VNPAY")
public class VnPayGateway implements PaymentGateway {
@Override
public PaymentResult process(PaymentRequest request) { ... }
@Override
public PaymentType getSupportedType() { return PaymentType.VNPAY; }
}
@Component("MOMO")
public class MoMoGateway implements PaymentGateway {
@Override
public PaymentResult process(PaymentRequest request) { ... }
@Override
public PaymentType getSupportedType() { return PaymentType.MOMO; }
}# dynamic Factory tự nối (không switch-case!)
@Service
public class EnterprisePaymentFactory {
private final Map<PaymentType, PaymentGateway> gatewayMap;
// Spring tự động thu gom toàn bộ các Bean implements PaymentGateway và truyền vào List
public EnterprisePaymentFactory(List<PaymentGateway> gateways) {
this.gatewayMap = gateways.stream()
.collect(Collectors.toUnmodifiableMap(
PaymentGateway::getSupportedType,
Function.identity()
));
}
public PaymentGateway getGateway(PaymentType type) {
PaymentGateway gateway = gatewayMap.get(type);
if (gateway == null) {
throw new UnsupportedPaymentException("Không tìm thấy bộ xử lý cho cổng: " + type);
}
return gateway;
}
}OCP đúng nghĩa: Khi cần thêm
ZaloPay, dev chỉ việc tạo file mớiZaloPayGateway.javagắn@Component("ZALOPAY"). Lúc khởi động, hệ thống tự phát hiện và nạp nó vào Factory. Factory không phải sửa dòng nào. (Vì key ở đây là enumPaymentType, bạn vẫn phải thêm một hằngZALOPAYvào enum. Muốn tuyệt đối không đụng file cũ thì dùng key kiểuString.)
Vài chi tiết đáng để ý trong cách làm này:
Collectors.toUnmodifiableMapnémIllegalStateExceptionnếu hai bean trả về cùng mộtPaymentType, nên lỡ copy-paste quên đổi type thì app chết ngay lúc khởi động, thay vì âm thầm chọn nhầm gateway lúc chạy. Fail sớm như vậy là tốt.- Spring cũng inject thẳng được
Map<String, PaymentGateway>, với key là tên bean (chính là chuỗi trong@Component("VNPAY")). Gọn hơn, nhưng định danh khi đó nằm trong annotation chứ không nằm trong code, dễ gõ sai mà compiler không bắt được. - Bean mặc định là singleton: mọi request dùng chung một instance
VnPayGateway, nên gateway phải stateless hoặc thread-safe. Nếu thật sự cần object mới cho mỗi lần gọi, dùng scopeprototypekèmObjectProvider<T>.
# functional Factory: dùng Java Supplier (không cần Spring)
Nếu bạn đang viết thư viện Java thuần (core SDK), không có Spring, thì làm sao có một Factory vừa linh hoạt vừa không phải tạo sẵn object từ đầu?
Dùng Map<K, Supplier<V>> kết hợp method reference:
public class FunctionalDocumentFactory {
private final Map<String, Supplier<DocumentParser>> registry = new ConcurrentHashMap<>();
public FunctionalDocumentFactory() {
// Đăng ký lazy constructor thông qua Method Reference
register("PDF", PdfDocumentParser::new);
register("DOCX", DocxDocumentParser::new);
register("EXCEL", ExcelDocumentParser::new);
}
public void register(String format, Supplier<DocumentParser> supplier) {
registry.put(format.toUpperCase(Locale.ROOT), Objects.requireNonNull(supplier));
}
public DocumentParser createParser(String format) {
Supplier<DocumentParser> supplier = registry.get(format.toUpperCase(Locale.ROOT));
if (supplier == null) {
throw new IllegalArgumentException("Định dạng tài liệu không hỗ trợ: " + format);
}
return supplier.get(); // Tạo instance mới chỉ khi được yêu cầu!
}
}Locale.ROOT ở đây không thừa: toUpperCase() không tham số dùng locale mặc định của máy, và trên máy đặt locale tiếng Thổ Nhĩ Kỳ thì "pdf" vẫn ổn nhưng chữ i sẽ thành İ (có chấm), khiến những key như "wiki" không bao giờ khớp.
# phân biệt Factory method và Abstract Factory
Hai cái này nhiều người hay nhầm, và còn hay nhầm cả với "Simple Factory" (một class có switch như phần đầu bài, vốn không nằm trong 23 pattern của GoF).
Factory Method đúng nghĩa GoF là một method trừu tượng ở lớp cha, để lớp con quyết định tạo object cụ thể nào. Ví dụ quen thuộc nhất là Iterable.iterator(): ArrayList trả về iterator của nó, HashSet trả về iterator khác, code gọi chỉ biết Iterator. Viết theo kiểu ứng dụng thì trông như sau:
public abstract class NotificationService {
// Factory Method: lớp con quyết định tạo kênh gửi nào
protected abstract NotificationChannel createChannel();
public void notify(User user, String message) {
createChannel().send(user, message);
}
}
public class SmsNotificationService extends NotificationService {
@Override
protected NotificationChannel createChannel() { return new SmsChannel(); }
}Ranh giới giữa chúng nhìn từ góc độ kiến trúc như sau:
# khi nào dùng Abstract Factory cho multi-cloud?
Ví dụ hệ thống phải chạy được trên cả AWS lẫn Google Cloud:
- Bạn không muốn code nghiệp vụ dính chặt vào AWS S3 hay Google Cloud Storage.
- Bạn định nghĩa
CloudFactorycó các method:createBlobStorage(),createMessageQueue(),createDatabase(). - Deploy lên AWS thì nạp
AwsCloudFactory→ blob, queue, DB đều là đồ của AWS, không có chuyện lẫn S3 của AWS với Pub/Sub của GCP!
# so sánh các biến thể Factory
| Pattern | Cách hoạt động | Mức tuân thủ OCP | Độ phức tạp | Dùng khi nào? |
|---|---|---|---|---|
| Simple Factory (Switch-Case) | Một class duy nhất chứa hết logic rẽ nhánh | ❌ Rất kém | Thấp | App đồ chơi, số type cố định mãi mãi (vd: các thứ trong tuần). |
| Spring Dynamic Factory | Tự gom bean qua List<T> hoặc Map<String, T> | 🟢 Tuyệt đối | Thấp (nhờ Spring) | Theo mình nên là lựa chọn mặc định trong dự án Spring Boot. |
| Functional Factory (Supplier) | Đăng ký lambda vào một registry map | 🟢 Tuyệt đối | Trung bình | Core library, SDK độc lập không phụ thuộc Spring. |
| Abstract Factory (GoF) | Tạo một họ object phụ thuộc lẫn nhau | 🟢 Rất cao | Cao | Hệ thống multi-cloud, GUI đa nền tảng, hỗ trợ nhiều loại DB. |
# checklist trước khi lên production (architect's checklist)
- 1. Bỏ switch-case viết cứng: Rà hết các class Factory, thay chuỗi
if-else / switchbằng Spring Map Injection hoặc Functional Registry. - 2. Mỗi component một định danh riêng: Bean nào implement interface cũng phải trả về một identifier duy nhất và không đổi.
- 3. Xử lý khi không tìm thấy (fallback): Factory nên ném custom business exception có ngữ cảnh rõ ràng (ví dụ
UnsupportedProviderException), đừng lặng lẽ trả vềnull. - 4. Đăng ký động lúc runtime: Nếu hệ thống có nạp plugin động (dynamic plugin loading), registry map phải dùng
ConcurrentHashMapđể thread-safe khi đăng ký thêm module. - 5. Theo dõi metric: Thêm metric đếm số lần gọi từng provider (ví dụ
payment.gateway.invocations{provider="VNPAY"}) để biết traffic thực tế.
# lời kết
Factory Pattern làm đúng thì không phải một mớ câu lệnh điều kiện. Nó là cách gói gọn chuyện khởi tạo object lại một chỗ, để hệ thống thêm loại mới thoải mái mà code đang chạy vẫn nằm yên không ai đụng tới.
Nếu dự án đang dùng Spring, thử rà xem có Factory nào còn switch dài không, rồi chuyển sang kiểu gom bean như trên. Còn nếu chỉ có ba bốn loại cố định thì cứ để switch, đừng dựng kiến trúc cho một bài toán chưa tồn tại.
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 😎 👍🏻 🚀 🔥.