Abstract Factory Pattern #
Ketika sebuah aplikasi harus berjalan di beberapa environment berbeda — misalnya AWS di production, GCP di staging, dan mock di unit test — masalah yang muncul bukan hanya soal satu objek yang berbeda, melainkan seluruh keluarga objek yang harus diganti sekaligus: storage, message queue, secret manager, monitoring, semuanya harus berasal dari provider yang sama dan bekerja secara konsisten satu sama lain. Mengganti satu per satu secara manual di banyak tempat bukan hanya melelahkan, tapi juga rawan inkonsistensi — kamu bisa tanpa sengaja menggunakan AWS S3 bersama GCP Pub/Sub. Abstract Factory Pattern menyelesaikan masalah ini dengan menyediakan satu titik untuk menukar seluruh keluarga objek sekaligus, tanpa menyentuh kode yang menggunakannya.
Apa itu Abstract Factory Pattern? #
Abstract Factory adalah creational design pattern yang menyediakan interface untuk membuat sekumpulan objek yang saling berkaitan, tanpa menentukan kelas konkretnya. Alih-alih membuat setiap objek secara individual, client meminta satu factory untuk membuat semua objek yang dibutuhkan — dan factory tersebut menjamin bahwa semua objek berasal dari “keluarga” yang sama dan kompatibel satu sama lain.
Perbedaan paling mendasar antara Abstract Factory dan Factory Method: Factory Method menangani satu produk, sedangkan Abstract Factory menangani keluarga produk. Satu NotificationFactory bisa membuat Sender, Formatter, sekaligus Logger — ketiganya berasal dari “keluarga” Email atau “keluarga” SMS, dan dijamin kompatibel.
Tiga properti yang mendefinisikan Abstract Factory:
- Keluarga objek dibuat bersama — satu factory, banyak produk yang terkait
- Konsistensi terjamin — tidak mungkin mencampur produk dari keluarga berbeda secara tidak sengaja
- Client tidak tahu concrete class — hanya berinteraksi dengan interface produk dan interface factory
flowchart TD
subgraph Client
C[NotificationService]
end
subgraph Abstract Layer
AF[NotificationFactory\ninterface]
AS[Sender\ninterface]
AFmt[Formatter\ninterface]
AL[AuditLogger\ninterface]
end
subgraph Email Family
EF[EmailFactory]
ES[EmailSender]
EFmt[EmailFormatter]
EL[EmailAuditLogger]
end
subgraph SMS Family
SF[SMSFactory]
SS[SMSSender]
SFmt[SMSFormatter]
SL[SMSAuditLogger]
end
C -->|hanya tahu| AF
AF --> EF
AF --> SF
EF -->|creates| ES
EF -->|creates| EFmt
EF -->|creates| EL
SF -->|creates| SS
SF -->|creates| SFmt
SF -->|creates| SL
ES & SS -->|implementasi| AS
EFmt & SFmt -->|implementasi| AFmt
EL & SL -->|implementasi| AL
Mengapa “Keluarga” Objek Penting #
Masalah yang dipecahkan Abstract Factory bukan soal membuat satu objek — itu sudah ditangani Factory Method. Masalahnya adalah menjamin konsistensi antar objek yang harus bekerja bersama.
Perhatikan skenario berikut tanpa Abstract Factory:
// ANTI-PATTERN: mencampur objek dari keluarga berbeda — tidak ada yang mencegah ini
type NotificationService struct {
sender Sender
formatter Formatter
logger AuditLogger
}
func NewNotificationService() *NotificationService {
return &NotificationService{
sender: &EmailSender{}, // dari keluarga Email
formatter: &SMSFormatter{}, // dari keluarga SMS — inkonsisten!
logger: &EmailAuditLogger{}, // dari keluarga Email
}
// Compiler tidak akan protes — tapi format SMS akan dikirim via Email
// Bug seperti ini sangat sulit di-debug
}
// BENAR: factory menjamin semua objek dari keluarga yang sama
func NewNotificationService(factory NotificationFactory) *NotificationService {
return &NotificationService{
sender: factory.CreateSender(),
formatter: factory.CreateFormatter(),
logger: factory.CreateAuditLogger(),
}
// Tidak mungkin mencampur — factory adalah satu-satunya pintu
}
Inkonsistensi semacam ini bisa sangat halus di sistem besar. Abstract Factory menghilangkan kemungkinan ini secara struktural, bukan hanya lewat konvensi.
Struktur dan Komponen #
Abstract Factory memiliki lebih banyak komponen dibanding Factory Method karena ia mengelola keluarga produk, bukan hanya satu produk. Memahami setiap lapisan membantu kamu membaca dan menulis implementasi dengan benar.
classDiagram
class NotificationFactory {
<<interface>>
+CreateSender() Sender
+CreateFormatter() Formatter
+CreateAuditLogger() AuditLogger
}
class Sender {
<<interface>>
+Send(to, message string) error
}
class Formatter {
<<interface>>
+Format(template string, data map) string
+FormatSubject(subject string) string
}
class AuditLogger {
<<interface>>
+Log(event NotificationEvent) error
}
class EmailFactory {
+smtpHost string
+apiKey string
+CreateSender() Sender
+CreateFormatter() Formatter
+CreateAuditLogger() AuditLogger
}
class SMSFactory {
+providerURL string
+accountSID string
+CreateSender() Sender
+CreateFormatter() Formatter
+CreateAuditLogger() AuditLogger
}
NotificationFactory <|.. EmailFactory
NotificationFactory <|.. SMSFactory
Sender <|.. EmailSender
Sender <|.. SMSSender
Formatter <|.. EmailFormatter
Formatter <|.. SMSFormatter
AuditLogger <|.. EmailAuditLogger
AuditLogger <|.. SMSAuditLogger
EmailFactory ..> EmailSender : creates
EmailFactory ..> EmailFormatter : creates
EmailFactory ..> EmailAuditLogger : creates
SMSFactory ..> SMSSender : creates
SMSFactory ..> SMSFormatter : creates
SMSFactory ..> SMSAuditLogger : creates
Lima komponen Abstract Factory:
| Komponen | Peran | Contoh |
|---|---|---|
| Abstract Factory | Interface yang mendefinisikan semua method pembuatan | NotificationFactory |
| Concrete Factory | Implementasi yang membuat satu keluarga produk | EmailFactory, SMSFactory |
| Abstract Product | Interface untuk setiap jenis produk | Sender, Formatter, AuditLogger |
| Concrete Product | Implementasi konkret dari setiap produk | EmailSender, SMSSender |
| Client | Menggunakan factory dan produk melalui interface saja | NotificationService |
Implementasi Lengkap: Notification System #
Mari bangun notification system yang mendukung Email dan SMS. Setiap channel membutuhkan tiga komponen: Sender untuk mengirim, Formatter untuk memformat pesan, dan AuditLogger untuk mencatat aktivitas.
Abstract Products #
Definisikan interface untuk setiap jenis produk yang akan dibuat factory. Ini adalah kontrak yang harus dipenuhi semua concrete product.
package notification
import "time"
// NotificationEvent merepresentasikan satu kejadian pengiriman notifikasi.
type NotificationEvent struct {
Channel string
Recipient string
Message string
SentAt time.Time
Success bool
Error string
}
// Sender bertanggung jawab mengirimkan pesan ke penerima.
type Sender interface {
Send(recipient, message string) error
ChannelName() string
}
// Formatter bertanggung jawab memformat konten pesan sesuai channel.
type Formatter interface {
FormatBody(template string, data map[string]string) string
FormatSubject(subject string) string // relevan untuk email, diabaikan untuk SMS
}
// AuditLogger mencatat semua aktivitas pengiriman untuk keperluan audit.
type AuditLogger interface {
Log(event NotificationEvent) error
GetLogs(recipient string) ([]NotificationEvent, error)
}
Abstract Factory #
Satu interface factory yang mendefinisikan semua produk dalam satu keluarga.
// NotificationFactory adalah abstract factory — mendefinisikan kontrak
// untuk membuat seluruh keluarga objek notifikasi.
type NotificationFactory interface {
CreateSender() Sender
CreateFormatter() Formatter
CreateAuditLogger() AuditLogger
}
Concrete Products: Keluarga Email #
package notification
import (
"fmt"
"net/smtp"
"strings"
"sync"
"time"
)
// EmailSender mengirimkan notifikasi melalui protokol SMTP.
type EmailSender struct {
smtpHost string
smtpPort int
username string
password string
fromAddr string
}
func (e *EmailSender) Send(recipient, message string) error {
addr := fmt.Sprintf("%s:%d", e.smtpHost, e.smtpPort)
auth := smtp.PlainAuth("", e.username, e.password, e.smtpHost)
body := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: Notification\r\n\r\n%s",
e.fromAddr, recipient, message)
if err := smtp.SendMail(addr, auth, e.fromAddr, []string{recipient}, []byte(body)); err != nil {
return fmt.Errorf("email send failed: %w", err)
}
return nil
}
func (e *EmailSender) ChannelName() string { return "email" }
// EmailFormatter memformat pesan dengan struktur yang sesuai untuk email.
type EmailFormatter struct{}
func (f *EmailFormatter) FormatBody(template string, data map[string]string) string {
result := template
for key, value := range data {
result = strings.ReplaceAll(result, "{{"+key+"}}", value)
}
// Email bisa punya konten lebih kaya — tambahkan header dan footer
return fmt.Sprintf("Dear Customer,\n\n%s\n\nRegards,\nSupport Team", result)
}
func (f *EmailFormatter) FormatSubject(subject string) string {
return fmt.Sprintf("[Notification] %s", subject)
}
// EmailAuditLogger mencatat log pengiriman email ke in-memory store.
// Dalam produksi, ini akan menulis ke database atau logging service.
type EmailAuditLogger struct {
mu sync.Mutex
logs []NotificationEvent
}
func (l *EmailAuditLogger) Log(event NotificationEvent) error {
l.mu.Lock()
defer l.mu.Unlock()
l.logs = append(l.logs, event)
fmt.Printf("[EMAIL AUDIT] %s -> %s (success: %v)\n",
event.SentAt.Format(time.RFC3339), event.Recipient, event.Success)
return nil
}
func (l *EmailAuditLogger) GetLogs(recipient string) ([]NotificationEvent, error) {
l.mu.Lock()
defer l.mu.Unlock()
var result []NotificationEvent
for _, log := range l.logs {
if log.Recipient == recipient {
result = append(result, log)
}
}
return result, nil
}
Concrete Products: Keluarga SMS #
// SMSSender mengirimkan notifikasi melalui SMS gateway.
type SMSSender struct {
providerURL string
accountSID string
authToken string
fromNumber string
}
func (s *SMSSender) Send(recipient, message string) error {
// Dalam implementasi nyata: panggil API provider (Twilio, Vonage, dll)
fmt.Printf("[SMS] Mengirim ke %s via %s: %s\n", recipient, s.providerURL, message)
return nil
}
func (s *SMSSender) ChannelName() string { return "sms" }
// SMSFormatter memformat pesan agar sesuai batasan SMS (160 karakter per segmen).
type SMSFormatter struct {
maxLength int
}
func (f *SMSFormatter) FormatBody(template string, data map[string]string) string {
result := template
for key, value := range data {
result = strings.ReplaceAll(result, "{{"+key+"}}", value)
}
// SMS tidak butuh header/footer — singkat dan to the point
if len(result) > f.maxLength {
return result[:f.maxLength-3] + "..."
}
return result
}
func (f *SMSFormatter) FormatSubject(subject string) string {
// SMS tidak punya subject — kembalikan string kosong
return ""
}
// SMSAuditLogger mencatat log pengiriman SMS.
type SMSAuditLogger struct {
mu sync.Mutex
logs []NotificationEvent
}
func (l *SMSAuditLogger) Log(event NotificationEvent) error {
l.mu.Lock()
defer l.mu.Unlock()
l.logs = append(l.logs, event)
fmt.Printf("[SMS AUDIT] %s -> %s (success: %v)\n",
event.SentAt.Format(time.RFC3339), event.Recipient, event.Success)
return nil
}
func (l *SMSAuditLogger) GetLogs(recipient string) ([]NotificationEvent, error) {
l.mu.Lock()
defer l.mu.Unlock()
var result []NotificationEvent
for _, log := range l.logs {
if log.Recipient == recipient {
result = append(result, log)
}
}
return result, nil
}
Concrete Factories #
Setiap factory mengumpulkan konfigurasi dan membuat seluruh keluarga produknya. Seluruh kerumitan konfigurasi tersembunyi di sini — client tidak pernah perlu tahu detail ini.
// EmailConfig menyimpan semua konfigurasi yang dibutuhkan email factory.
type EmailConfig struct {
SMTPHost string
SMTPPort int
Username string
Password string
FromAddr string
}
// EmailFactory membuat seluruh keluarga objek notifikasi berbasis email.
type EmailFactory struct {
config EmailConfig
logger *EmailAuditLogger // shared logger dalam satu factory instance
}
func NewEmailFactory(cfg EmailConfig) *EmailFactory {
return &EmailFactory{
config: cfg,
logger: &EmailAuditLogger{},
}
}
func (f *EmailFactory) CreateSender() Sender {
return &EmailSender{
smtpHost: f.config.SMTPHost,
smtpPort: f.config.SMTPPort,
username: f.config.Username,
password: f.config.Password,
fromAddr: f.config.FromAddr,
}
}
func (f *EmailFactory) CreateFormatter() Formatter {
return &EmailFormatter{}
}
func (f *EmailFactory) CreateAuditLogger() AuditLogger {
return f.logger // semua email sender berbagi satu logger
}
// SMSConfig menyimpan semua konfigurasi yang dibutuhkan SMS factory.
type SMSConfig struct {
ProviderURL string
AccountSID string
AuthToken string
FromNumber string
MaxLength int
}
// SMSFactory membuat seluruh keluarga objek notifikasi berbasis SMS.
type SMSFactory struct {
config SMSConfig
logger *SMSAuditLogger
}
func NewSMSFactory(cfg SMSConfig) *SMSFactory {
maxLen := cfg.MaxLength
if maxLen == 0 {
maxLen = 160 // standar SMS
}
return &SMSFactory{
config: cfg,
logger: &SMSAuditLogger{},
}
}
func (f *SMSFactory) CreateSender() Sender {
return &SMSSender{
providerURL: f.config.ProviderURL,
accountSID: f.config.AccountSID,
authToken: f.config.AuthToken,
fromNumber: f.config.FromNumber,
}
}
func (f *SMSFactory) CreateFormatter() Formatter {
return &SMSFormatter{maxLength: f.config.MaxLength}
}
func (f *SMSFactory) CreateAuditLogger() AuditLogger {
return f.logger
}
Client Code #
Client menggunakan NotificationFactory tanpa pernah menyebut satu pun concrete type. Mengganti dari Email ke SMS cukup dengan mengganti factory yang diinjeksikan.
// NotificationService adalah client yang menggunakan abstract factory.
type NotificationService struct {
factory NotificationFactory
sender Sender
formatter Formatter
logger AuditLogger
}
func NewNotificationService(factory NotificationFactory) *NotificationService {
return &NotificationService{
factory: factory,
sender: factory.CreateSender(),
formatter: factory.CreateFormatter(),
logger: factory.CreateAuditLogger(),
}
}
// Send mengirimkan notifikasi dan mencatat hasilnya ke audit log.
func (s *NotificationService) Send(recipient, template string, data map[string]string) error {
body := s.formatter.FormatBody(template, data)
err := s.sender.Send(recipient, body)
event := NotificationEvent{
Channel: s.sender.ChannelName(),
Recipient: recipient,
Message: body,
SentAt: time.Now(),
Success: err == nil,
}
if err != nil {
event.Error = err.Error()
}
_ = s.logger.Log(event) // audit tetap dicatat meski send gagal
return err
}
// SendWelcome adalah contoh method bisnis yang tidak tahu channel apapun.
func (s *NotificationService) SendWelcome(recipient, username string) error {
return s.Send(
recipient,
"Selamat datang, {{name}}! Akun kamu sudah aktif.",
map[string]string{"name": username},
)
}
func (s *NotificationService) SendOTP(recipient, otp string) error {
return s.Send(
recipient,
"Kode OTP kamu adalah {{otp}}. Berlaku 5 menit.",
map[string]string{"otp": otp},
)
}
Penggunaan dari main.go:
func main() {
// Konfigurasi dibaca dari environment atau config file
emailCfg := notification.EmailConfig{
SMTPHost: "smtp.gmail.com",
SMTPPort: 587,
Username: os.Getenv("SMTP_USER"),
Password: os.Getenv("SMTP_PASS"),
FromAddr: "[email protected]",
}
smsCfg := notification.SMSConfig{
ProviderURL: "https://api.twilio.com",
AccountSID: os.Getenv("TWILIO_SID"),
AuthToken: os.Getenv("TWILIO_TOKEN"),
FromNumber: "+628001234567",
MaxLength: 160,
}
// Pilih factory di satu tempat — seluruh keluarga objek ikut berganti
var factory notification.NotificationFactory
if os.Getenv("NOTIFICATION_CHANNEL") == "sms" {
factory = notification.NewSMSFactory(smsCfg)
} else {
factory = notification.NewEmailFactory(emailCfg)
}
svc := notification.NewNotificationService(factory)
// Kode bisnis tidak tahu apakah ini email atau SMS
svc.SendWelcome("[email protected]", "Budi")
svc.SendOTP("[email protected]", "847291")
}
Studi Kasus Kedua: Cloud Provider Abstraction #
Notification system adalah contoh yang relatif sederhana. Abstract Factory benar-benar bersinar di skenario yang lebih kompleks seperti abstraksi cloud provider, di mana satu “keluarga” bisa terdiri dari belasan komponen yang saling tergantung.
// CloudFactory adalah abstract factory untuk resource cloud.
// Satu factory mewakili satu cloud provider — semua resource-nya kompatibel.
type CloudFactory interface {
CreateObjectStorage() ObjectStorage
CreateMessageQueue() MessageQueue
CreateSecretManager() SecretManager
CreateMetricsCollector() MetricsCollector
}
// ObjectStorage adalah abstract product untuk penyimpanan objek.
type ObjectStorage interface {
Put(bucket, key string, data []byte) error
Get(bucket, key string) ([]byte, error)
Delete(bucket, key string) error
ListKeys(bucket, prefix string) ([]string, error)
}
// MessageQueue adalah abstract product untuk antrian pesan.
type MessageQueue interface {
Publish(topic string, payload []byte) error
Subscribe(topic string, handler func([]byte) error) error
}
// SecretManager adalah abstract product untuk manajemen secret.
type SecretManager interface {
GetSecret(name string) (string, error)
SetSecret(name, value string) error
}
// MetricsCollector adalah abstract product untuk pengiriman metrik.
type MetricsCollector interface {
Counter(name string, tags map[string]string) error
Histogram(name string, value float64, tags map[string]string) error
}
Dengan struktur ini, mengganti seluruh infrastruktur cloud cukup dengan mengganti factory:
// Di production: semua resource dari AWS
var factory CloudFactory = NewAWSFactory(awsConfig)
// Di staging: semua resource dari GCP
var factory CloudFactory = NewGCPFactory(gcpConfig)
// Di unit test: semua resource dari mock
var factory CloudFactory = NewMockCloudFactory()
// Kode bisnis tidak berubah sama sekali
app := NewApplication(factory)
app.Run()
flowchart LR
subgraph Production
AF1[AWSFactory] --> AS[S3Storage]
AF1 --> AQ[SQSQueue]
AF1 --> ASM[AWSSecretsManager]
AF1 --> AM[CloudWatch]
end
subgraph Staging
GF[GCPFactory] --> GS[GCSStorage]
GF --> GQ[PubSubQueue]
GF --> GSM[SecretManagerGCP]
GF --> GM[StackdriverMetrics]
end
subgraph Testing
MF[MockFactory] --> MS[InMemoryStorage]
MF --> MQ[InMemoryQueue]
MF --> MSM[InMemorySecrets]
MF --> MM[NoopMetrics]
end
App[Application] -->|prod| AF1
App -->|staging| GF
App -->|test| MF
Abstract Factory vs Factory Method #
Kedua pattern ini sering membingungkan karena namanya mirip dan keduanya berkaitan dengan pembuatan objek. Perbedaannya fundamental:
// Factory Method — satu produk, satu method
type PaymentFactory interface {
CreatePayment() Payment // satu produk
}
// Abstract Factory — keluarga produk, banyak method
type InfrastructureFactory interface {
CreateStorage() Storage // produk pertama
CreateQueue() MessageQueue // produk kedua
CreateCache() Cache // produk ketiga
CreateLogger() DistributedLogger // produk keempat
// semua harus kompatibel satu sama lain
}
Cara mudah membedakannya: jika kamu hanya perlu satu Create...(), gunakan Factory Method. Jika kamu perlu beberapa Create...() yang produknya harus kompatibel satu sama lain, gunakan Abstract Factory.
| Aspek | Factory Method | Abstract Factory |
|---|---|---|
| Jumlah produk | Satu | Banyak (keluarga) |
| Interface factory | Satu method | Banyak method |
| Masalah yang dipecahkan | Fleksibilitas pembuatan satu objek | Konsistensi antar objek yang saling terkait |
| Kompleksitas | Lebih rendah | Lebih tinggi |
| Kapan digunakan | Variasi implementasi satu konsep | Variasi seluruh stack implementasi |
| Contoh | NewPayment(method) |
NewInfrastructure(provider) |
Testing dengan Mock Factory #
Abstract Factory membuat testing sangat bersih. Kamu bisa membuat MockFactory yang mengembalikan mock produk, dan seluruh suite test berjalan tanpa dependency eksternal.
// MockNotificationFactory untuk testing — menggantikan Email atau SMS factory
type MockNotificationFactory struct {
sender *MockSender
formatter *MockFormatter
logger *MockAuditLogger
}
func NewMockNotificationFactory() *MockNotificationFactory {
return &MockNotificationFactory{
sender: &MockSender{},
formatter: &MockFormatter{},
logger: &MockAuditLogger{},
}
}
func (f *MockNotificationFactory) CreateSender() Sender { return f.sender }
func (f *MockNotificationFactory) CreateFormatter() Formatter { return f.formatter }
func (f *MockNotificationFactory) CreateAuditLogger() AuditLogger { return f.logger }
// MockSender merekam panggilan untuk di-assert di test
type MockSender struct {
SentMessages []struct{ Recipient, Message string }
ShouldFail bool
}
func (m *MockSender) Send(recipient, message string) error {
if m.ShouldFail {
return errors.New("mock send failure")
}
m.SentMessages = append(m.SentMessages, struct{ Recipient, Message string }{recipient, message})
return nil
}
func (m *MockSender) ChannelName() string { return "mock" }
// MockFormatter mengembalikan string yang predictable untuk assertion
type MockFormatter struct{}
func (f *MockFormatter) FormatBody(template string, data map[string]string) string {
return template // kembalikan template apa adanya untuk testing
}
func (f *MockFormatter) FormatSubject(subject string) string { return subject }
// MockAuditLogger merekam log untuk di-assert di test
type MockAuditLogger struct {
Events []NotificationEvent
}
func (l *MockAuditLogger) Log(event NotificationEvent) error {
l.Events = append(l.Events, event)
return nil
}
func (l *MockAuditLogger) GetLogs(recipient string) ([]NotificationEvent, error) {
return l.Events, nil
}
// Test bersih — tidak ada SMTP, tidak ada jaringan
func TestNotificationService_SendWelcome(t *testing.T) {
mockFactory := NewMockNotificationFactory()
svc := NewNotificationService(mockFactory)
err := svc.SendWelcome("[email protected]", "Budi")
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
// Assert pesan dikirim
if len(mockFactory.sender.SentMessages) != 1 {
t.Errorf("expected 1 message sent, got %d", len(mockFactory.sender.SentMessages))
}
if mockFactory.sender.SentMessages[0].Recipient != "[email protected]" {
t.Errorf("wrong recipient")
}
// Assert audit log dicatat
if len(mockFactory.logger.Events) != 1 {
t.Errorf("expected 1 audit event, got %d", len(mockFactory.logger.Events))
}
if !mockFactory.logger.Events[0].Success {
t.Errorf("expected success=true in audit log")
}
}
func TestNotificationService_SendWelcome_WhenSenderFails(t *testing.T) {
mockFactory := NewMockNotificationFactory()
mockFactory.sender.ShouldFail = true
svc := NewNotificationService(mockFactory)
err := svc.SendWelcome("[email protected]", "Budi")
if err == nil {
t.Error("expected error when sender fails")
}
// Audit log tetap dicatat meski gagal
if len(mockFactory.logger.Events) != 1 {
t.Errorf("expected audit log even on failure")
}
if mockFactory.logger.Events[0].Success {
t.Errorf("expected success=false in audit log")
}
}
Jangan Buat Factory yang Terlalu Besar
Jika abstract factory kamu memiliki lebih dari 5-7 method
Create...(), itu sinyal kuat bahwa factory tersebut mencoba melakukan terlalu banyak. Pertimbangkan untuk memecah menjadi beberapa factory yang lebih kecil dan terfokus — misalnyaStorageFactorydanMessagingFactoryterpisah, alih-alih satuInfrastructureFactorymonolitik.
Kapan Menggunakan dan Kapan Tidak #
Abstract Factory adalah pattern yang powerful, tapi bukan solusi untuk semua masalah. Penggunaannya yang berlebihan menghasilkan kode yang kompleks tanpa manfaat nyata.
GUNAKAN Abstract Factory jika:
✓ Ada beberapa "keluarga" implementasi yang akan saling ditukar
✓ Objek dalam satu keluarga harus kompatibel dan konsisten
✓ Client tidak boleh tahu concrete class sama sekali
✓ Ada kemungkinan menambah keluarga baru di masa depan
✓ System testing membutuhkan seluruh keluarga di-mock sekaligus
HINDARI Abstract Factory jika:
✗ Hanya ada satu implementasi dan tidak ada rencana menambah
✗ Produk dalam "keluarga" tidak saling tergantung
✗ Kompleksitas abstraksi melebihi manfaat yang didapat
✗ Tim kecil dengan timeline ketat — mulai simple, refactor saat butuh
✗ Setiap factory method hanya punya satu concrete implementation
Mulai Simple, Refactor Saat Butuh
Jika kamu belum yakin butuh Abstract Factory, mulai dengan Simple Factory atau bahkan konstruktor biasa. Ketika kamu mulai melihat pattern “kumpulan objek yang harus diganti bersama” — itulah saat yang tepat untuk refactor ke Abstract Factory. Premature abstraction lebih berbahaya daripada refactoring di kemudian hari.
Checklist Review Abstract Factory #
DESAIN:
□ Ada abstract factory interface dengan lebih dari satu Create method
□ Semua produk dalam satu keluarga saling kompatibel
□ Client hanya bergantung pada abstract factory dan abstract product
□ Tidak ada cara untuk mencampur produk dari keluarga berbeda
IMPLEMENTASI:
□ Setiap concrete factory membuat seluruh keluarga produknya sendiri
□ Konfigurasi diinjeksikan ke factory, bukan ke concrete product
□ Factory tidak berisi business logic — hanya object creation
□ Error dari proses pembuatan ditangani dengan jelas
KONSISTENSI:
□ Semua concrete factory mengimplementasikan seluruh method di interface
□ Produk dari satu factory bisa bekerja bersama tanpa konfigurasi tambahan
□ Menambah keluarga baru tidak mengubah kode client sama sekali
TESTING:
□ Ada mock factory yang mengembalikan mock produk
□ Test suite berjalan tanpa dependency eksternal
□ Edge case (factory gagal, produk mengembalikan error) di-test
□ Audit trail (logger) tetap di-test bahkan untuk skenario gagal
Ringkasan #
- Abstract Factory mengelola keluarga objek — berbeda dengan Factory Method yang mengelola satu objek; di sini satu factory bertanggung jawab atas banyak produk yang harus kompatibel.
- Konsistensi adalah keunggulan utama — tidak ada cara untuk mencampur produk dari keluarga berbeda secara tidak sengaja; inkonsistensi seperti EmailSender dengan SMSFormatter menjadi mustahil secara struktural.
- Lima komponen: Abstract Factory, Concrete Factory, Abstract Product, Concrete Product, dan Client — masing-masing punya peran yang jelas dan tidak tumpang tindih.
- Client sepenuhnya terisolasi dari concrete class; mengganti seluruh keluarga implementasi cukup dengan mengganti factory yang diinjeksikan — tidak ada kode lain yang berubah.
- Testing menjadi sangat bersih — Mock Factory menggantikan seluruh keluarga produk sekaligus; tidak ada SMTP, tidak ada HTTP call, tidak ada database.
- Bedakan dengan Factory Method: gunakan Factory Method untuk satu produk dengan variasi implementasi, gunakan Abstract Factory untuk beberapa produk yang harus konsisten dalam satu keluarga.
- Jangan over-engineer — mulai dari konstruktor biasa atau Factory Method, refactor ke Abstract Factory hanya ketika muncul kebutuhan nyata untuk menukar seluruh keluarga objek.
- Factory yang besar adalah sinyal bahaya — lebih dari 5-7 method
Create...()berarti factory mencoba terlalu banyak; pecah menjadi beberapa factory yang lebih terfokus.