abstract factory pattern
Tung Bui Thanh (TungDaDev)9 mins read

Nhiều bài giảng giới thiệu Abstract Factory bằng ví dụ vẽ giao diện Light Theme vs Dark Theme (Button, Checkbox). Nhưng làm backend thì mấy khi phải viết UI Button. Chỗ Abstract Factory thật sự đáng dùng là bài toán multi-cloud: làm sao để phần core business chạy được trên AWS, GCP hay data center on-premise mà không bị trói vào SDK riêng của nhà cung cấp nào (vendor lock-in)?
Nếu code của bạn gọi thẳng AmazonS3Client, AmazonSQSAsync, AWSSecretsManager (SDK v1) hay S3Client, SqsClient (SDK v2) rải khắp hàng trăm Service class, thì tới hôm sếp quyết chuyển sang Google Cloud (để nhận gói tài trợ triệu đô) hoặc khách hàng chính phủ đòi chạy on-premise, cả dự án sẽ kẹt cứng, viết lại thì tốn không biết bao nhiêu mà kể.
Phần dưới dùng Abstract Factory Pattern để dựng một lớp trừu tượng hạ tầng cloud (Cloud Infrastructure Abstraction Layer) đủ dùng cho production.
# ý tưởng chính: một "gia đình" sản phẩm đi chung với nhau
Factory Method chỉ lo tạo một object. Abstract Factory thì đưa ra một interface để tạo cả một "gia đình" object liên quan chặt với nhau:
# sao phải gom thành một "gia đình"?
- Cho mọi thứ khớp nhau: app đang chạy trên AWS thì lưu file, bắn message, lấy mật khẩu đều phải dùng hạ tầng AWS, để còn tận dụng mạng nội bộ (AWS VPC), IAM Roles và đỡ tiền đẩy dữ liệu ra ngoài (Data Egress Cost).
- Tránh trộn lẫn: lưu file lên AWS S3 mà lại lấy secret từ Google Cloud thì về kỹ thuật vẫn làm được (qua workload identity federation), nhưng cấu hình quyền và mạng rối gấp đôi mà chẳng được gì. Abstract Factory ép cả bộ phải cùng một nhà.
# thiết kế các interface trừu tượng cho cloud
Trước tiên, định nghĩa các "hợp đồng" (contract) mà tầng nghiệp vụ cần:
// 1. Quản lý lưu trữ đối tượng nhị phân (Object / Blob Storage)
public interface BlobStorage {
void upload(String bucketName, String objectKey, byte[] data);
byte[] download(String bucketName, String objectKey);
}
// 2. Hàng đợi thông điệp bất đồng bộ (Message Queue)
public interface MessageQueue {
void publish(String topicOrQueue, String payload);
void subscribe(String topicOrQueue, Consumer<String> handler);
}
// 3. Quản lý bí mật và mật khẩu (Secrets Management)
public interface SecretManager {
String getSecret(String secretName);
}Rồi tới Abstract Factory interface:
public interface CloudInfrastructureFactory {
BlobStorage createBlobStorage();
MessageQueue createMessageQueue();
SecretManager createSecretManager();
}# cài đặt cho AWS và google cloud (GCP)
# concrete Factory cho AWS
public class AwsInfrastructureFactory implements CloudInfrastructureFactory {
private final AwsCredentialsProvider credentialsProvider;
private final Region region;
public AwsInfrastructureFactory(AwsCredentialsProvider credentialsProvider, Region region) {
this.credentialsProvider = credentialsProvider;
this.region = region;
}
@Override
public BlobStorage createBlobStorage() {
S3Client s3Client = S3Client.builder()
.region(region)
.credentialsProvider(credentialsProvider)
.build();
return new AwsS3BlobStorage(s3Client);
}
@Override
public MessageQueue createMessageQueue() {
SqsClient sqsClient = SqsClient.builder()
.region(region)
.credentialsProvider(credentialsProvider)
.build();
return new AwsSqsMessageQueue(sqsClient);
}
@Override
public SecretManager createSecretManager() {
SecretsManagerClient smClient = SecretsManagerClient.builder()
.region(region)
.credentialsProvider(credentialsProvider)
.build();
return new AwsSecretManagerImpl(smClient);
}
}# concrete Factory cho google cloud (GCP)
public class GcpInfrastructureFactory implements CloudInfrastructureFactory {
private final GoogleCredentials credentials;
private final String projectId;
public GcpInfrastructureFactory(GoogleCredentials credentials, String projectId) {
this.credentials = credentials;
this.projectId = projectId;
}
@Override
public BlobStorage createBlobStorage() {
Storage storage = StorageOptions.newBuilder()
.setCredentials(credentials)
.setProjectId(projectId)
.build()
.getService();
return new GcpBlobStorage(storage);
}
@Override
public MessageQueue createMessageQueue() {
// Khởi tạo Publisher / Subscriber của Google Cloud Pub/Sub
return new GcpPubSubQueue(projectId, credentials);
}
@Override
public SecretManager createSecretManager() {
// Khởi tạo GCP Secret Manager Client
return new GcpSecretManagerImpl(projectId, credentials);
}
}# gắn vào Spring Boot: chọn cloud bằng config
Khỏi tạo Factory bằng tay. Với Spring Boot, mình kết hợp Abstract Factory với @ConditionalOnProperty:
@Configuration
public class CloudInfrastructureConfiguration {
@Bean
@ConditionalOnProperty(name = "cloud.provider", havingValue = "aws")
public CloudInfrastructureFactory awsInfrastructureFactory(
@Value("${cloud.aws.region:ap-southeast-1}") String regionStr) {
return new AwsInfrastructureFactory(DefaultCredentialsProvider.builder().build(), Region.of(regionStr));
}
@Bean
@ConditionalOnProperty(name = "cloud.provider", havingValue = "gcp")
public CloudInfrastructureFactory gcpInfrastructureFactory(
@Value("${cloud.gcp.project-id}") String projectId) throws IOException {
return new GcpInfrastructureFactory(GoogleCredentials.getApplicationDefault(), projectId);
}
// Tự động phân phối các Beans thành phần cho toàn bộ hệ thống
@Bean
public BlobStorage blobStorage(CloudInfrastructureFactory factory) {
return factory.createBlobStorage();
}
@Bean
public MessageQueue messageQueue(CloudInfrastructureFactory factory) {
return factory.createMessageQueue();
}
@Bean
public SecretManager secretManager(CloudInfrastructureFactory factory) {
return factory.createSecretManager();
}
}# dùng ở tầng nghiệp vụ: không dính gì tới cloud!
@Service
public class DocumentManagementService {
private final BlobStorage blobStorage;
private final MessageQueue messageQueue;
public DocumentManagementService(BlobStorage blobStorage, MessageQueue messageQueue) {
this.blobStorage = blobStorage;
this.messageQueue = messageQueue;
}
public void processInvoice(String invoiceId, byte[] pdfData) {
// Lưu trữ file: Chạy trên AWS sẽ vào S3, chạy trên GCP sẽ vào Google Cloud Storage
blobStorage.upload("company-invoices", invoiceId + ".pdf", pdfData);
// Bắn sự kiện: Chạy trên AWS sẽ vào SQS, chạy trên GCP sẽ vào Pub/Sub
messageQueue.publish("invoice-processed-events", "{\"invoiceId\":\"" + invoiceId + "\"}");
}
}# cái giá phải trả: "mẫu số chung nhỏ nhất" (lowest common denominator)
Chẳng có giải pháp nào hoàn hảo. Dùng Abstract Factory để giấu hạ tầng cloud đi thì bạn sẽ gặp ngay bài toán mẫu số chung nhỏ nhất:
# đánh đổi thật sự
- Mất tính năng riêng: gộp S3 và GCS vào chung một interface
BlobStoragethì bạn không dùng được mấy tính năng xịn riêng của từng bên (như S3 Glacier Instant Retrieval hay GCP Turbo Replication), trừ khi chấp nhận làm thủng lớp trừu tượng. - Ngữ nghĩa khác nhau nằm dưới cùng một method:
MessageQueue.subscribe()trông giống nhau, nhưng SQS là queue kéo (pull), mỗi message chỉ tới một consumer, visibility timeout tối đa 12 tiếng; còn Pub/Sub là topic có nhiều subscription, mỗi subscription nhận một bản.Consumer<String>cũng không cho biết khi nào ack, xử lý lỗi thì retry hay đẩy sang dead-letter. Interface chung mà không nói rõ những chuyện này thì đổi cloud xong code vẫn chạy, nhưng hành vi lặng lẽ đổi theo. - Tốn công bảo trì: team Platform phải tự giữ và test lớp Adapter cho từng cloud provider mỗi lần SDK của họ lên version.
Trước khi tự viết cả lớp trừu tượng, cũng nên xem có chuẩn chung sẵn chưa. Nhiều nơi hỗ trợ API tương thích S3 (MinIO, Cloudflare R2, và cả GCS qua XML API với HMAC key), nên đôi khi chỉ cần đổi endpoint. Phần messaging thì Spring Cloud Stream đã có binder cho Kafka, RabbitMQ, Pub/Sub...
Lời khuyên khi thiết kế: chỉ dùng Abstract Factory cho các dịch vụ nền tảng mà các bên giống nhau nhiều (Object Storage, Message Queues, RDBMS). Còn dịch vụ đặc thù (như BigQuery của Google hay Amazon Bedrock AI của AWS) thì cứ gọi thẳng, đừng cố trừu tượng hoá cho bằng được.
# checklist trước khi lên production (architect's checklist)
- 1. Kiểm tra rò rỉ dependency: module
domain-coretuyệt đối không import package nào củasoftware.amazon.awssdkhaycom.google.cloud. Mọi dependency cloud phải nằm trong moduleinfrastructure. - 2. Có Local Mock Factory: làm một
LocalStorageFactory(lưu file xuống ổ cứng, dùng hàng đợi trong bộ nhớLinkedBlockingQueue) để dev chạy và unit test mà không cần tài khoản cloud thật. - 3. Thống nhất cách xử lý lỗi: exception riêng của AWS (
S3Exception) và GCP (StorageException) phải được các lớp cài đặt (Concrete Product nhưAwsS3BlobStorage) bọc lại thành domain exception chung (ví dụStorageUnavailableException). - 4. Quản lý vòng đời connection: các client do Factory tạo ra (S3Client, PubSub) phải được đóng đàng hoàng (
close()/@PreDestroy) khi app tắt. Spring tự gọiclose()cho bean@Beancó method này, nhưng ở đây bean là lớp bọcBlobStoragechứ không phảiS3Client, nên lớp bọc cần implementAutoCloseablevà đóng client bên trong. - 5. Theo dõi metrics theo từng cloud: gắn Micrometer Tags
cloud_provider: "aws"hoặccloud_provider: "gcp"để xem độ trễ SLA của từng cloud trên Grafana.
# lời kết
Abstract Factory Pattern đâu phải chỉ để vẽ nút bấm UI. Dùng đúng chỗ, nó giúp công ty giảm vendor lock-in, thứ có thể tốn cả triệu đô, và cho hệ thống chuyển qua lại giữa các cloud nhẹ nhàng hơn nhiều.
Có điều đừng làm vì "biết đâu sau này". Nếu không có kế hoạch multi-cloud hay on-premise thật, một interface mỏng cho phần bạn cần test (để mock) là đủ; dựng cả bộ factory cho mọi dịch vụ mà chỉ chạy một cloud thì chỉ tốn thêm code để bảo trì.
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 😎 👍🏻 🚀 🔥.