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 implementasi —
Payment, bukanCreditCardPayment - 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.RoundTripperadalah 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.