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 — misalnya StorageFactory dan MessagingFactory terpisah, alih-alih satu InfrastructureFactory monolitik.


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.

← Sebelumnya: Factory Method   Berikutnya: Builder →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact