Factory Method Pattern #

Setiap kali kamu menulis if paymentType == "credit_card" { ... } else if paymentType == "ewallet" { ... }, kamu sedang menyatukan dua hal yang seharusnya terpisah: keputusan objek apa yang dibuat dan bagaimana objek itu digunakan. Semakin banyak tipe yang ditambahkan, semakin panjang blok kondisional ini, dan semakin banyak tempat di codebase yang harus diubah setiap kali ada penambahan tipe baru. Factory Method Pattern hadir untuk memotong siklus ini — ia memindahkan keputusan pembuatan objek ke satu tempat yang terisolasi, sehingga kode yang menggunakan objek tidak perlu tahu dan tidak perlu berubah ketika tipe baru ditambahkan.

Apa itu Factory Method Pattern? #

Factory Method adalah creational design pattern yang mendefinisikan sebuah interface atau method untuk membuat objek, tetapi menyerahkan keputusan class atau struct mana yang akan di-instansiasi kepada implementasi konkretnya. Kode yang membutuhkan objek hanya berinteraksi dengan interface — ia tidak pernah memanggil &CreditCardPayment{} atau &EWalletPayment{} secara langsung.

Tiga properti inti yang mendefinisikan Factory Method:

  • Kode client hanya tahu interface, tidak tahu implementasiPayment, bukan CreditCardPayment
  • Proses pembuatan objek dienkapsulasi dalam factory — creation logic ada di satu tempat, bukan tersebar
  • Menambah tipe baru tidak mengubah kode client — ini adalah Open-Closed Principle dalam praktik nyata

Perbedaan paling penting antara Factory Method dan sekadar fungsi konstruktor biasa adalah adanya lapisan abstraksi. Fungsi NewPayment(type string) masih mengharuskan client menyebut tipe secara eksplisit. Factory Method membalik tanggung jawab ini — client memilih factory yang tepat, dan factory yang memutuskan objek apa yang dibuat.

flowchart TD
    subgraph Client
        C[ProcessPayment\nfunc]
    end

    subgraph Factory Layer
        direction TB
        FI[PaymentFactory\ninterface]
        CCF[CreditCardFactory]
        EWF[EWalletFactory]
        BNF[BankTransferFactory]
    end

    subgraph Product Layer
        direction TB
        PI[Payment\ninterface]
        CCP[CreditCardPayment]
        EWP[EWalletPayment]
        BNP[BankTransferPayment]
    end

    C -->|hanya tahu| FI
    C -->|hanya tahu| PI
    FI --> CCF
    FI --> EWF
    FI --> BNF
    CCF -->|membuat| CCP
    EWF -->|membuat| EWP
    BNF -->|membuat| BNP
    CCP -->|implementasi| PI
    EWP -->|implementasi| PI
    BNP -->|implementasi| PI

Masalah yang Diselesaikan #

Sebelum melihat implementasi, penting memahami masalah konkret yang terjadi tanpa Factory Method.

Masalah: Object Creation Tersebar di Mana-mana #

Bayangkan sebuah sistem e-commerce yang menangani payment di banyak tempat: checkout flow, refund handler, subscription renewal, dan invoice generator. Tanpa factory, setiap tempat ini akan punya kode pembuatan objek sendiri:

// ANTI-PATTERN: creation logic tersebar di mana-mana

// Di checkout handler
func (h *CheckoutHandler) Process(req CheckoutRequest) error {
    var payment Payment
    if req.Method == "credit_card" {
        payment = &CreditCardPayment{APIKey: "sk_live_xxx"}
    } else if req.Method == "ewallet" {
        payment = &EWalletPayment{MerchantID: "M123"}
    } else if req.Method == "bank_transfer" {
        payment = &BankTransferPayment{BankCode: "BCA"}
    }
    return payment.Pay(req.Amount)
}

// Di refund handler — kode yang SAMA diulang lagi
func (h *RefundHandler) Process(req RefundRequest) error {
    var payment Payment
    if req.Method == "credit_card" {
        payment = &CreditCardPayment{APIKey: "sk_live_xxx"} // duplikasi
    } else if req.Method == "ewallet" {
        payment = &EWalletPayment{MerchantID: "M123"}       // duplikasi
    }
    return payment.Refund(req.Amount)
}

// Ketika BankTransfer ditambahkan ke refund, harus ubah semua tempat

Ketika ada payment method baru — misalnya QRIS — developer harus mencari dan mengubah setiap if/else di seluruh codebase. Ini rapuh dan mudah terlewat.

// BENAR: creation logic terpusat di factory

// Di checkout handler — bersih, tidak tahu konkret class apapun
func (h *CheckoutHandler) Process(factory PaymentFactory, req CheckoutRequest) error {
    payment := factory.CreatePayment()
    return payment.Pay(req.Amount)
}

// Di refund handler — sama persis, tidak ada duplikasi
func (h *RefundHandler) Process(factory PaymentFactory, req RefundRequest) error {
    payment := factory.CreatePayment()
    return payment.Refund(req.Amount)
}

// Tambah QRIS? Cukup buat QRISFactory — tidak ada kode lain yang berubah

Struktur dan Komponen #

Factory Method terdiri dari empat komponen yang bekerja bersama. Memahami peran masing-masing memudahkan kamu membaca dan menulis implementasi yang benar.

classDiagram
    class PaymentFactory {
        <<interface>>
        +CreatePayment() Payment
    }

    class Payment {
        <<interface>>
        +Pay(amount int) error
        +Refund(amount int) error
        +GetStatus(txID string) Status
    }

    class CreditCardFactory {
        +apiKey string
        +CreatePayment() Payment
    }

    class EWalletFactory {
        +merchantID string
        +CreatePayment() Payment
    }

    class CreditCardPayment {
        +apiKey string
        +Pay(amount int) error
        +Refund(amount int) error
        +GetStatus(txID string) Status
    }

    class EWalletPayment {
        +merchantID string
        +Pay(amount int) error
        +Refund(amount int) error
        +GetStatus(txID string) Status
    }

    PaymentFactory <|.. CreditCardFactory
    PaymentFactory <|.. EWalletFactory
    Payment <|.. CreditCardPayment
    Payment <|.. EWalletPayment
    CreditCardFactory ..> CreditCardPayment : creates
    EWalletFactory ..> EWalletPayment : creates

Empat komponen:

Komponen Peran Contoh
Product Interface Kontrak yang harus dipenuhi semua produk Payment
Concrete Product Implementasi nyata dari interface CreditCardPayment, EWalletPayment
Factory Interface Kontrak untuk semua factory PaymentFactory
Concrete Factory Membuat concrete product tertentu CreditCardFactory, EWalletFactory

Implementasi Lengkap di Golang #

Golang tidak punya class atau inheritance, tapi justru interface-nya yang implisit membuat Factory Method sangat natural untuk diimplementasikan. Mari bangun payment system secara bertahap.

Langkah 1: Definisikan Product Interface #

Product interface adalah kontrak yang harus dipenuhi oleh semua implementasi pembayaran. Client hanya akan berinteraksi dengan interface ini.

package payment

import "fmt"

// Status merepresentasikan kondisi sebuah transaksi pembayaran.
type Status string

const (
    StatusPending  Status = "pending"
    StatusSuccess  Status = "success"
    StatusFailed   Status = "failed"
    StatusRefunded Status = "refunded"
)

// Payment adalah product interface — semua metode pembayaran harus memenuhi kontrak ini.
type Payment interface {
    Pay(amount int) error
    Refund(transactionID string, amount int) error
    GetStatus(transactionID string) (Status, error)
    MethodName() string
}

Langkah 2: Buat Concrete Products #

Setiap metode pembayaran mengimplementasikan interface Payment dengan caranya sendiri. Detail implementasinya bisa sangat berbeda — CreditCard memanggil API eksternal, EWallet mungkin punya flow redirect, BankTransfer menunggu konfirmasi manual.

// CreditCardPayment menangani transaksi via kartu kredit.
type CreditCardPayment struct {
    apiKey     string
    merchantID string
}

func (c *CreditCardPayment) Pay(amount int) error {
    fmt.Printf("[CreditCard] Memproses pembayaran Rp%d via API key %s\n", amount, c.apiKey[:8]+"***")
    // Dalam implementasi nyata: panggil payment gateway API
    return nil
}

func (c *CreditCardPayment) Refund(transactionID string, amount int) error {
    fmt.Printf("[CreditCard] Refund Rp%d untuk transaksi %s\n", amount, transactionID)
    return nil
}

func (c *CreditCardPayment) GetStatus(transactionID string) (Status, error) {
    // Dalam implementasi nyata: query ke payment gateway
    return StatusSuccess, nil
}

func (c *CreditCardPayment) MethodName() string {
    return "credit_card"
}


// EWalletPayment menangani transaksi via dompet digital.
type EWalletPayment struct {
    merchantID  string
    callbackURL string
}

func (e *EWalletPayment) Pay(amount int) error {
    fmt.Printf("[EWallet] Membuat payment link Rp%d untuk merchant %s\n", amount, e.merchantID)
    // Dalam implementasi nyata: buat payment URL, redirect user
    return nil
}

func (e *EWalletPayment) Refund(transactionID string, amount int) error {
    fmt.Printf("[EWallet] Meminta refund Rp%d untuk transaksi %s\n", amount, transactionID)
    return nil
}

func (e *EWalletPayment) GetStatus(transactionID string) (Status, error) {
    return StatusPending, nil
}

func (e *EWalletPayment) MethodName() string {
    return "ewallet"
}


// BankTransferPayment menangani transfer bank manual.
type BankTransferPayment struct {
    bankCode      string
    accountNumber string
}

func (b *BankTransferPayment) Pay(amount int) error {
    fmt.Printf("[BankTransfer] Menampilkan VA %s bank %s untuk Rp%d\n",
        b.accountNumber, b.bankCode, amount)
    return nil
}

func (b *BankTransferPayment) Refund(transactionID string, amount int) error {
    fmt.Printf("[BankTransfer] Refund manual Rp%d — perlu konfirmasi tim finance\n", amount)
    return nil
}

func (b *BankTransferPayment) GetStatus(transactionID string) (Status, error) {
    return StatusPending, nil
}

func (b *BankTransferPayment) MethodName() string {
    return "bank_transfer"
}

Langkah 3: Definisikan Factory Interface #

Factory interface mendefinisikan kontrak untuk semua factory. Client hanya akan memegang reference ke interface ini.

// PaymentFactory adalah factory interface — semua factory harus bisa membuat Payment.
type PaymentFactory interface {
    CreatePayment() Payment
}

Langkah 4: Buat Concrete Factories #

Setiap factory bertanggung jawab atas pembuatan satu jenis payment, termasuk menyediakan semua konfigurasi yang dibutuhkan.

// Config menyimpan semua konfigurasi yang dibutuhkan factory.
type Config struct {
    CreditCardAPIKey    string
    CreditCardMerchantID string
    EWalletMerchantID   string
    EWalletCallbackURL  string
    BankCode            string
    BankAccountNumber   string
}

// CreditCardFactory membuat instance CreditCardPayment yang sudah terkonfigurasi.
type CreditCardFactory struct {
    config Config
}

func NewCreditCardFactory(cfg Config) *CreditCardFactory {
    return &CreditCardFactory{config: cfg}
}

func (f *CreditCardFactory) CreatePayment() Payment {
    return &CreditCardPayment{
        apiKey:     f.config.CreditCardAPIKey,
        merchantID: f.config.CreditCardMerchantID,
    }
}


// EWalletFactory membuat instance EWalletPayment yang sudah terkonfigurasi.
type EWalletFactory struct {
    config Config
}

func NewEWalletFactory(cfg Config) *EWalletFactory {
    return &EWalletFactory{config: cfg}
}

func (f *EWalletFactory) CreatePayment() Payment {
    return &EWalletPayment{
        merchantID:  f.config.EWalletMerchantID,
        callbackURL: f.config.EWalletCallbackURL,
    }
}


// BankTransferFactory membuat instance BankTransferPayment.
type BankTransferFactory struct {
    config Config
}

func NewBankTransferFactory(cfg Config) *BankTransferFactory {
    return &BankTransferFactory{config: cfg}
}

func (f *BankTransferFactory) CreatePayment() Payment {
    return &BankTransferPayment{
        bankCode:      f.config.BankCode,
        accountNumber: f.config.BankAccountNumber,
    }
}

Langkah 5: Kode Client yang Bersih #

Client code bekerja sepenuhnya melalui interface — tidak ada satu pun concrete type yang disebutkan.

// OrderService menggunakan PaymentFactory tanpa tahu implementasi konkretnya.
type OrderService struct {
    paymentFactory PaymentFactory
}

func NewOrderService(factory PaymentFactory) *OrderService {
    return &OrderService{paymentFactory: factory}
}

func (s *OrderService) Checkout(orderID string, amount int) error {
    payment := s.paymentFactory.CreatePayment()
    fmt.Printf("Checkout order %s menggunakan %s\n", orderID, payment.MethodName())
    return payment.Pay(amount)
}

func (s *OrderService) ProcessRefund(orderID, transactionID string, amount int) error {
    payment := s.paymentFactory.CreatePayment()
    return payment.Refund(transactionID, amount)
}

Penggunaan dari main.go atau layer di atas service:

func main() {
    cfg := payment.Config{
        CreditCardAPIKey:     "sk_live_abcdefgh",
        CreditCardMerchantID: "M_CC_001",
        EWalletMerchantID:    "M_EW_001",
        EWalletCallbackURL:   "https://myapp.com/payment/callback",
        BankCode:             "BCA",
        BankAccountNumber:    "1234567890",
    }

    // Pilih factory berdasarkan konfigurasi atau request user
    factories := map[string]payment.PaymentFactory{
        "credit_card":   payment.NewCreditCardFactory(cfg),
        "ewallet":       payment.NewEWalletFactory(cfg),
        "bank_transfer": payment.NewBankTransferFactory(cfg),
    }

    // Dispatch ke factory yang tepat
    selectedMethod := "ewallet" // misalnya dari request body
    factory, ok := factories[selectedMethod]
    if !ok {
        log.Fatalf("metode pembayaran tidak dikenal: %s", selectedMethod)
    }

    svc := NewOrderService(factory)
    if err := svc.Checkout("ORDER-001", 150000); err != nil {
        log.Fatal(err)
    }
}

Simple Factory vs Factory Method #

Di Golang, sangat umum menemukan dua pendekatan yang sering disebut “factory” padahal berbeda secara fundamental. Penting memahami perbedaannya agar kamu memilih yang tepat.

Simple Factory: Fungsi Switch #

Simple Factory adalah fungsi tunggal yang menerima parameter dan me-return implementasi yang sesuai. Ini bukan Factory Method Pattern — ini adalah fungsi helper dengan logika kondisional.

// Simple Factory — satu fungsi dengan switch
func NewPayment(method string, cfg Config) (Payment, error) {
    switch method {
    case "credit_card":
        return &CreditCardPayment{
            apiKey:     cfg.CreditCardAPIKey,
            merchantID: cfg.CreditCardMerchantID,
        }, nil
    case "ewallet":
        return &EWalletPayment{
            merchantID:  cfg.EWalletMerchantID,
            callbackURL: cfg.EWalletCallbackURL,
        }, nil
    case "bank_transfer":
        return &BankTransferPayment{
            bankCode:      cfg.BankCode,
            accountNumber: cfg.BankAccountNumber,
        }, nil
    default:
        return nil, fmt.Errorf("metode pembayaran tidak dikenal: %s", method)
    }
}

Penggunaan Simple Factory:

// Client masih menyebut tipe secara eksplisit melalui string
payment, err := NewPayment("credit_card", cfg)

Perbandingan Langsung #

Aspek Simple Factory Factory Method
Menambah tipe baru Ubah fungsi switch yang sudah ada Buat factory baru, tidak ubah yang lama
Open-Closed Principle Dilanggar — harus modifikasi fungsi Dipenuhi — extension tanpa modification
Testability Sulit mock — factory adalah fungsi Mudah mock — factory adalah interface
Kompleksitas Rendah — satu fungsi Lebih tinggi — banyak struct
Cocok untuk Tipe stabil, sistem kecil Tipe sering bertambah, sistem modular
flowchart TD
    subgraph "Simple Factory"
        SF["NewPayment(method)"]
        SF -->|"credit_card"| SFA[CreditCardPayment]
        SF -->|"ewallet"| SFB[EWalletPayment]
        SF -->|"bank_transfer"| SFC[BankTransferPayment]
        SF -.->|"tambah QRIS?\nubah fungsi ini"| SFD[???]
    end

    subgraph "Factory Method"
        FMI[PaymentFactory\ninterface]
        FMI --> FMA[CreditCardFactory]
        FMI --> FMB[EWalletFactory]
        FMI --> FMC[BankTransferFactory]
        FMI --> FMD[QRISFactory\ntambah tanpa\nubah yang lain]
    end

Golang Idiom: Hybrid Approach

Di Golang yang pragmatis, sering digunakan hybrid — Simple Factory untuk dispatch ke factory yang tepat, Factory Method untuk implementation. Registry pattern adalah contoh umum:

var factoryRegistry = map[string]PaymentFactory{}

func RegisterFactory(method string, factory PaymentFactory) {
    factoryRegistry[method] = factory
}

func GetFactory(method string) (PaymentFactory, error) {
    f, ok := factoryRegistry[method]
    if !ok {
        return nil, fmt.Errorf("factory untuk '%s' tidak terdaftar", method)
    }
    return f, nil
}

Dengan ini, menambah payment method baru cukup dengan RegisterFactory("qris", NewQRISFactory(cfg)) — tidak ada kode lain yang diubah.


Testing dengan Factory Method #

Salah satu keunggulan terbesar Factory Method adalah kemudahan testing. Karena client bergantung pada interface, kamu bisa inject mock factory tanpa mengubah kode production.

// MockPaymentFactory untuk testing — tidak perlu library eksternal
type MockPaymentFactory struct {
    payment Payment
}

func (m *MockPaymentFactory) CreatePayment() Payment {
    return m.payment
}

// MockPayment untuk testing
type MockPayment struct {
    payError    error
    refundError error
    status      Status
    methodName  string
}

func (m *MockPayment) Pay(amount int) error          { return m.payError }
func (m *MockPayment) Refund(txID string, amount int) error { return m.refundError }
func (m *MockPayment) GetStatus(txID string) (Status, error) { return m.status, nil }
func (m *MockPayment) MethodName() string            { return m.methodName }


// Test OrderService tanpa menyentuh payment gateway sungguhan
func TestOrderService_Checkout_Success(t *testing.T) {
    mockPayment := &MockPayment{
        payError:   nil,
        methodName: "mock",
    }
    mockFactory := &MockPaymentFactory{payment: mockPayment}

    svc := NewOrderService(mockFactory)
    err := svc.Checkout("ORDER-TEST-001", 100000)

    if err != nil {
        t.Errorf("expected nil error, got %v", err)
    }
}

func TestOrderService_Checkout_PaymentFailed(t *testing.T) {
    mockPayment := &MockPayment{
        payError:   errors.New("insufficient balance"),
        methodName: "mock",
    }
    mockFactory := &MockPaymentFactory{payment: mockPayment}

    svc := NewOrderService(mockFactory)
    err := svc.Checkout("ORDER-TEST-002", 500000)

    if err == nil {
        t.Error("expected error, got nil")
    }
}

Test di atas tidak pernah menyentuh API eksternal, tidak membutuhkan koneksi jaringan, dan berjalan dalam milidetik. Ini adalah keuntungan langsung dari desain berbasis interface.


Factory Method di Ekosistem Golang #

Factory Method bukan hanya teori — ia muncul di mana-mana dalam ekosistem Golang standar dan library populer.

database/sql — Driver Selection #

// sql.Open adalah factory yang memilih driver berdasarkan nama
db, err := sql.Open("postgres", dsn)  // PostgreSQL driver
db, err := sql.Open("mysql", dsn)     // MySQL driver
db, err := sql.Open("sqlite3", dsn)   // SQLite driver

// Client (sql.Open) tidak tahu implementasi driver — hanya tahu sql.Driver interface

log/slog — Handler Selection #

// Pilih handler (factory) tanpa mengubah cara logging
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))    // JSON output
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))    // Text output
// Custom handler: implementasikan slog.Handler interface, selesai

HTTP Transport #

// http.RoundTripper adalah product interface
// Berbagai transport mengimplementasikannya
client := &http.Client{
    Transport: &http.Transport{...},        // default TCP transport
    // atau
    Transport: &CustomRetryTransport{...},  // custom dengan retry logic
    // atau
    Transport: &MockTransport{...},         // mock untuk testing
}

Kapan Menggunakan Masing-masing Pendekatan #

Memilih antara Simple Factory dan Factory Method (atau bahkan tidak menggunakan keduanya) bergantung pada karakteristik konkret sistem yang sedang dibangun.

GUNAKAN konstruktor biasa (func New...) jika:
  ✓ Hanya ada satu implementasi
  ✓ Tidak ada rencana untuk menambah variasi
  ✓ Creation logic sangat sederhana (satu baris)

GUNAKAN Simple Factory jika:
  ✓ Ada beberapa implementasi tetapi jumlahnya stabil
  ✓ Tim kecil, iterasi cepat
  ✓ Sistem masih dalam fase eksplorasi
  ✗ Jangan jika switch sudah punya lebih dari 5-7 case

GUNAKAN Factory Method jika:
  ✓ Tipe implementasi akan terus bertambah seiring waktu
  ✓ Third-party harus bisa menambahkan implementasi mereka sendiri
  ✓ Testing dan mocking adalah prioritas
  ✓ Plugin-based atau extensible architecture
  ✓ Tim besar dengan kontrak interface yang jelas antar tim

Checklist Review Factory Method #

DESAIN:
  □ Ada product interface yang jelas (bukan concrete type)
  □ Ada factory interface yang terpisah dari product interface
  □ Client code hanya bergantung pada interface, tidak concrete type
  □ Setiap factory bertanggung jawab atas satu jenis produk

IMPLEMENTASI:
  □ Factory mengembalikan interface, bukan concrete struct
  □ Error dari proses pembuatan ditangani dengan baik
  □ Konfigurasi diinjeksikan ke factory, bukan ke concrete product langsung
  □ Tidak ada business logic di dalam factory — hanya creation

EKSTENSIBILITAS:
  □ Menambah tipe baru tidak memerlukan perubahan di kode client
  □ Tidak ada switch atau if-else berbasis tipe di kode client
  □ Open-Closed Principle terpenuhi

TESTING:
  □ Ada mock factory untuk keperluan unit test
  □ Semua test berjalan tanpa dependency eksternal
  □ Edge case (factory gagal, config salah) di-test

Ringkasan #

  • Factory Method memisahkan “apa yang dibuat” dari “bagaimana digunakan” — client hanya tahu interface, bukan implementasi konkret.
  • Empat komponen utama: Product Interface, Concrete Product, Factory Interface, Concrete Factory — pahami peran masing-masing sebelum menulis kode.
  • Simple Factory vs Factory Method: gunakan Simple Factory untuk sistem kecil dengan tipe stabil, gunakan Factory Method ketika tipe akan terus bertambah dan extensibility adalah prioritas.
  • Open-Closed Principle: menambah payment method baru cukup dengan membuat factory baru — tidak ada kode lain yang diubah.
  • Testing menjadi trivial: karena client bergantung pada interface, mock factory bisa diinjeksikan tanpa menyentuh production code sama sekali.
  • Golang ekosistem menggunakannya luas: database/sql, log/slog, http.RoundTripper adalah contoh Factory Method yang kamu gunakan setiap hari.
  • Registry pattern adalah idiom Golang yang menggabungkan Simple Factory (untuk dispatch) dengan Factory Method (untuk implementation) — fleksibel dan mudah di-extend.
  • Jangan overkill: untuk sistem kecil dengan satu implementasi, konstruktor biasa sudah cukup; Factory Method bersinar ketika variasi dan extensibility nyata dibutuhkan.

← Sebelumnya: Singleton   Berikutnya: Abstract Factory →

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