State Pattern #

Sebuah pesanan di e-commerce melewati banyak tahap: dibuat, menunggu pembayaran, dibayar, dikemas, dikirim, tiba, dan selesai — atau bisa dibatalkan kapan saja sebelum dikirim, atau dikembalikan setelah tiba. Setiap tahap memiliki operasi yang berbeda: pesanan yang menunggu pembayaran bisa dibayar atau dibatalkan, tapi tidak bisa dikemas; pesanan yang sudah dikirim tidak bisa dibatalkan, tapi bisa dilacak. Tanpa State Pattern, OrderService akan dipenuhi kondisi seperti if order.Status == "pending_payment" { ... } else if order.Status == "paid" { ... } — dan setiap operasi harus memeriksa status sebelum melakukan apapun. State Pattern memindahkan logika ini ke dalam objek state — setiap state tahu operasi apa yang valid dan bagaimana bertransisi ke state berikutnya — sehingga OrderService tidak perlu tahu apa-apa tentang aturan transisi.

Apa itu State Pattern? #

State Pattern adalah behavioral design pattern yang memungkinkan sebuah objek mengubah perilakunya ketika state internalnya berubah, seolah-olah objek tersebut mengganti tipe-nya saat runtime. Alih-alih menggunakan kondisional if-else atau switch yang terus berkembang, setiap state direpresentasikan sebagai objek terpisah yang mengimplementasikan behavior untuk state tersebut.

Perbedaan mendasar State Pattern dari sekadar “enum status”:

  • Behavior berubah sesuai state — bukan hanya label yang berubah
  • Transisi dikontrol — setiap state menentukan ke state mana ia bisa berpindah; transisi yang tidak valid menghasilkan error, bukan silent bug
  • State tahu context — state bisa memicu transisi ke state lain melalui Context, tanpa client perlu tahu mekanismenya
stateDiagram-v2
    [*] --> PendingPayment : Order dibuat

    PendingPayment --> Paid : Pay()
    PendingPayment --> Cancelled : Cancel()

    Paid --> Processing : StartProcessing()
    Paid --> Cancelled : Cancel()

    Processing --> Shipped : Ship()

    Shipped --> Delivered : ConfirmDelivery()
    Shipped --> ReturnRequested : RequestReturn()

    Delivered --> ReturnRequested : RequestReturn()
    ReturnRequested --> Returned : ProcessReturn()

    Cancelled --> [*]
    Returned --> [*]
    Delivered --> [*]

Masalah yang Dipecahkan #

State Pattern menyelesaikan dua masalah yang muncul bersama: logika kondisional yang bertumpuk dan transisi state yang tidak terkontrol.

Masalah 1: if-else yang Tidak Bisa Dikendalikan #

// ANTI-PATTERN: semua logika state tersebar di satu struct
func (o *Order) Pay(amount float64) error {
    switch o.Status {
    case "pending_payment":
        if amount < o.TotalAmount {
            return fmt.Errorf("insufficient payment")
        }
        o.Status = "paid"
        o.PaidAt = time.Now()
        return nil
    case "paid":
        return fmt.Errorf("order already paid")
    case "cancelled":
        return fmt.Errorf("cannot pay cancelled order")
    default:
        return fmt.Errorf("cannot pay order in status: %s", o.Status)
    }
}

func (o *Order) Cancel() error {
    switch o.Status {
    case "pending_payment", "paid":
        o.Status = "cancelled"
        o.CancelledAt = time.Now()
        return nil
    case "shipped":
        return fmt.Errorf("cannot cancel shipped order")
    // ... setiap operasi punya switch sendiri yang harus selalu konsisten
    }
}

// Masalah: menambah status baru berarti update SEMUA switch di SEMUA method
// Jika satu switch lupa diupdate → bug yang sangat sulit dideteksi

// BENAR: setiap state tahu aturannya sendiri
func (o *Order) Pay(amount float64) error {
    return o.currentState.Pay(o, amount) // delegasi ke state aktif
}

Masalah 2: Transisi yang Tidak Terkontrol #

// ANTI-PATTERN: siapapun bisa set status ke apapun
order.Status = "shipped" // bisa dilakukan dari mana saja, melewati validasi

// BENAR: transisi hanya melalui method yang dikontrol
order.Ship() // state memvalidasi apakah transisi ini valid dari state saat ini

Tiga Komponen State Pattern #

classDiagram
    class OrderState {
        <<interface>>
        +Pay(ctx *Order, amount float64) error
        +Cancel(ctx *Order) error
        +StartProcessing(ctx *Order) error
        +Ship(ctx *Order) error
        +ConfirmDelivery(ctx *Order) error
        +RequestReturn(ctx *Order) error
        +StateName() string
    }

    class Order {
        -state OrderState
        -id string
        -totalAmount float64
        -history []StateTransition
        +Pay(amount float64) error
        +Cancel() error
        +Ship() error
        +CurrentState() string
        +setState(state OrderState)
    }

    class PendingPaymentState {
        +Pay(ctx *Order, amount float64) error
        +Cancel(ctx *Order) error
        +StateName() string
    }

    class PaidState {
        +StartProcessing(ctx *Order) error
        +Cancel(ctx *Order) error
        +StateName() string
    }

    class ShippedState {
        +ConfirmDelivery(ctx *Order) error
        +RequestReturn(ctx *Order) error
        +StateName() string
    }

    OrderState <|.. PendingPaymentState
    OrderState <|.. PaidState
    OrderState <|.. ShippedState
    Order o-- OrderState : currentState
Komponen Peran Di Golang
State interface Mendefinisikan semua operasi yang bisa terjadi di setiap state Interface dengan semua method yang mungkin
Concrete State Mengimplementasikan behavior untuk state tertentu; menolak operasi yang tidak valid Struct yang mengimplementasikan interface
Context Menyimpan referensi ke state aktif; mendelegasikan operasi ke state Struct Order dengan field currentState

Implementasi Lengkap: Order Lifecycle #

State Interface dan Context #

package order

import (
    "fmt"
    "time"
)

// StateTransition mencatat setiap perubahan state untuk audit.
type StateTransition struct {
    From      string
    To        string
    At        time.Time
    Reason    string
}

// OrderState mendefinisikan semua operasi yang bisa dilakukan pada pesanan.
// Setiap state mengimplementasikan interface ini; operasi yang tidak valid
// mengembalikan error dengan pesan yang informatif.
type OrderState interface {
    Pay(ctx *Order, amount float64) error
    Cancel(ctx *Order, reason string) error
    StartProcessing(ctx *Order) error
    Ship(ctx *Order, trackingNumber, courier string) error
    ConfirmDelivery(ctx *Order) error
    RequestReturn(ctx *Order, reason string) error
    ProcessReturn(ctx *Order) error
    StateName() string
}

// Order adalah Context — menyimpan state aktif dan mendelegasikan semua operasi.
type Order struct {
    ID            string
    UserID        string
    TotalAmount   float64
    PaidAmount    float64
    Items         []OrderItem
    TrackingNumber string
    Courier       string

    state         OrderState
    history       []StateTransition

    // Timestamps
    CreatedAt    time.Time
    PaidAt       *time.Time
    ShippedAt    *time.Time
    DeliveredAt  *time.Time
    CancelledAt  *time.Time
}

// OrderItem merepresentasikan satu item dalam pesanan.
type OrderItem struct {
    ProductID string
    Quantity  int
    Price     float64
}

// NewOrder membuat pesanan baru dalam state PendingPayment.
func NewOrder(id, userID string, items []OrderItem, totalAmount float64) *Order {
    o := &Order{
        ID:          id,
        UserID:      userID,
        Items:       items,
        TotalAmount: totalAmount,
        CreatedAt:   time.Now(),
    }
    o.state = &PendingPaymentState{}
    return o
}

// setState mengganti state aktif dan mencatat transisi ke history.
// Method ini dipanggil oleh concrete state, bukan oleh client.
func (o *Order) setState(newState OrderState) {
    transition := StateTransition{
        From: o.state.StateName(),
        To:   newState.StateName(),
        At:   time.Now(),
    }
    o.history = append(o.history, transition)
    o.state = newState
}

// CurrentState mengembalikan nama state aktif.
func (o *Order) CurrentState() string { return o.state.StateName() }

// History mengembalikan riwayat transisi state.
func (o *Order) History() []StateTransition { return o.history }

// --- Public methods yang mendelegasikan ke state aktif --- //

func (o *Order) Pay(amount float64) error {
    return o.state.Pay(o, amount)
}

func (o *Order) Cancel(reason string) error {
    return o.state.Cancel(o, reason)
}

func (o *Order) StartProcessing() error {
    return o.state.StartProcessing(o)
}

func (o *Order) Ship(trackingNumber, courier string) error {
    return o.state.Ship(o, trackingNumber, courier)
}

func (o *Order) ConfirmDelivery() error {
    return o.state.ConfirmDelivery(o)
}

func (o *Order) RequestReturn(reason string) error {
    return o.state.RequestReturn(o, reason)
}

func (o *Order) ProcessReturn() error {
    return o.state.ProcessReturn(o)
}

BaseState: Default “Operasi Tidak Valid” #

// BaseState menyediakan implementasi default untuk semua operasi.
// Setiap operasi mengembalikan error "invalid transition".
// Concrete state hanya perlu override method yang valid untuk state tersebut.
type BaseState struct{}

func (b *BaseState) Pay(ctx *Order, amount float64) error {
    return fmt.Errorf("cannot pay order in state: %s", ctx.CurrentState())
}

func (b *BaseState) Cancel(ctx *Order, reason string) error {
    return fmt.Errorf("cannot cancel order in state: %s", ctx.CurrentState())
}

func (b *BaseState) StartProcessing(ctx *Order) error {
    return fmt.Errorf("cannot start processing order in state: %s", ctx.CurrentState())
}

func (b *BaseState) Ship(ctx *Order, trackingNumber, courier string) error {
    return fmt.Errorf("cannot ship order in state: %s", ctx.CurrentState())
}

func (b *BaseState) ConfirmDelivery(ctx *Order) error {
    return fmt.Errorf("cannot confirm delivery in state: %s", ctx.CurrentState())
}

func (b *BaseState) RequestReturn(ctx *Order, reason string) error {
    return fmt.Errorf("cannot request return in state: %s", ctx.CurrentState())
}

func (b *BaseState) ProcessReturn(ctx *Order) error {
    return fmt.Errorf("cannot process return in state: %s", ctx.CurrentState())
}

Concrete States #

// PendingPaymentState — menunggu pembayaran.
// Operasi valid: Pay, Cancel.
type PendingPaymentState struct{ BaseState }

func (s *PendingPaymentState) Pay(ctx *Order, amount float64) error {
    if amount < ctx.TotalAmount {
        return fmt.Errorf("payment insufficient: need Rp %.0f, got Rp %.0f",
            ctx.TotalAmount, amount)
    }

    now := time.Now()
    ctx.PaidAmount = amount
    ctx.PaidAt = &now
    ctx.setState(&PaidState{})
    return nil
}

func (s *PendingPaymentState) Cancel(ctx *Order, reason string) error {
    now := time.Now()
    ctx.CancelledAt = &now
    ctx.setState(&CancelledState{Reason: reason})
    return nil
}

func (s *PendingPaymentState) StateName() string { return "pending_payment" }


// PaidState — pembayaran sudah dikonfirmasi, menunggu diproses.
// Operasi valid: StartProcessing, Cancel.
type PaidState struct{ BaseState }

func (s *PaidState) StartProcessing(ctx *Order) error {
    ctx.setState(&ProcessingState{})
    return nil
}

func (s *PaidState) Cancel(ctx *Order, reason string) error {
    // Pembayaran sudah masuk — perlu refund
    now := time.Now()
    ctx.CancelledAt = &now
    ctx.setState(&CancelledState{Reason: reason, NeedsRefund: true})
    return nil
}

func (s *PaidState) StateName() string { return "paid" }


// ProcessingState — sedang dikemas/diproses.
// Operasi valid: Ship.
type ProcessingState struct{ BaseState }

func (s *ProcessingState) Ship(ctx *Order, trackingNumber, courier string) error {
    if trackingNumber == "" {
        return fmt.Errorf("tracking number is required to ship order")
    }
    if courier == "" {
        return fmt.Errorf("courier name is required to ship order")
    }

    now := time.Now()
    ctx.TrackingNumber = trackingNumber
    ctx.Courier = courier
    ctx.ShippedAt = &now
    ctx.setState(&ShippedState{})
    return nil
}

func (s *ProcessingState) StateName() string { return "processing" }


// ShippedState — sudah dikirim, dalam perjalanan.
// Operasi valid: ConfirmDelivery, RequestReturn.
type ShippedState struct{ BaseState }

func (s *ShippedState) ConfirmDelivery(ctx *Order) error {
    now := time.Now()
    ctx.DeliveredAt = &now
    ctx.setState(&DeliveredState{})
    return nil
}

func (s *ShippedState) RequestReturn(ctx *Order, reason string) error {
    ctx.setState(&ReturnRequestedState{Reason: reason})
    return nil
}

func (s *ShippedState) StateName() string { return "shipped" }


// DeliveredState — sudah diterima pembeli.
// Operasi valid: RequestReturn (dalam batas waktu tertentu).
type DeliveredState struct{ BaseState }

func (s *DeliveredState) RequestReturn(ctx *Order, reason string) error {
    if ctx.DeliveredAt == nil {
        return fmt.Errorf("delivery timestamp not set")
    }
    // Return hanya boleh dalam 7 hari setelah diterima
    if time.Since(*ctx.DeliveredAt) > 7*24*time.Hour {
        return fmt.Errorf("return window has expired (7 days after delivery)")
    }
    ctx.setState(&ReturnRequestedState{Reason: reason})
    return nil
}

func (s *DeliveredState) StateName() string { return "delivered" }


// ReturnRequestedState — return sudah diminta, menunggu diproses.
// Operasi valid: ProcessReturn.
type ReturnRequestedState struct {
    BaseState
    Reason string
}

func (s *ReturnRequestedState) ProcessReturn(ctx *Order) error {
    ctx.setState(&ReturnedState{})
    return nil
}

func (s *ReturnRequestedState) StateName() string { return "return_requested" }


// ReturnedState — barang sudah dikembalikan. Terminal state.
type ReturnedState struct{ BaseState }

func (s *ReturnedState) StateName() string { return "returned" }


// CancelledState — pesanan dibatalkan. Terminal state.
type CancelledState struct {
    BaseState
    Reason      string
    NeedsRefund bool
}

func (s *CancelledState) StateName() string { return "cancelled" }

Demonstrasi: Order yang Melalui Full Lifecycle #

func main() {
    items := []order.OrderItem{
        {ProductID: "PRD-001", Quantity: 2, Price: 150000},
        {ProductID: "PRD-002", Quantity: 1, Price: 75000},
    }

    o := order.NewOrder("ORD-001", "user-123", items, 375000)
    fmt.Printf("Created: %s\n", o.CurrentState()) // pending_payment

    // Happy path: pembayaran → proses → kirim → terima
    if err := o.Pay(375000); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("After pay: %s\n", o.CurrentState()) // paid

    if err := o.StartProcessing(); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("After processing: %s\n", o.CurrentState()) // processing

    if err := o.Ship("JNE1234567890", "JNE"); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("After ship: %s (tracking: %s)\n", o.CurrentState(), o.TrackingNumber)

    if err := o.ConfirmDelivery(); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("After delivery: %s\n", o.CurrentState()) // delivered

    // Coba operasi yang tidak valid — state menolak dengan pesan jelas
    if err := o.Cancel("ubah pikiran"); err != nil {
        fmt.Printf("Cannot cancel: %v\n", err)
        // "cannot cancel order in state: delivered"
    }

    // Tampilkan history transisi
    fmt.Println("\nOrder History:")
    for _, t := range o.History() {
        fmt.Printf("  %s → %s (%s)\n", t.From, t.To, t.At.Format("15:04:05"))
    }
}

Guard Condition: Validasi Sebelum Transisi #

Guard condition adalah pemeriksaan tambahan yang harus dipenuhi sebelum transisi state diizinkan — di luar pemeriksaan state saat ini.

// PendingPaymentState dengan guard condition yang lebih lengkap
func (s *PendingPaymentState) Pay(ctx *Order, amount float64) error {
    // Guard 1: jumlah pembayaran
    if amount <= 0 {
        return fmt.Errorf("payment amount must be positive")
    }
    if amount < ctx.TotalAmount {
        return fmt.Errorf("insufficient payment: expected Rp %.0f, got Rp %.0f",
            ctx.TotalAmount, amount)
    }

    // Guard 2: pesanan masih dalam batas waktu pembayaran (24 jam)
    paymentDeadline := ctx.CreatedAt.Add(24 * time.Hour)
    if time.Now().After(paymentDeadline) {
        // Auto-cancel dan transisi ke state cancelled
        ctx.setState(&CancelledState{Reason: "payment timeout", NeedsRefund: false})
        return fmt.Errorf("payment deadline exceeded: order automatically cancelled")
    }

    // Guard 3: items masih tersedia (dalam implementasi nyata: cek inventory)
    // if !inventoryAvailable(ctx.Items) {
    //     return fmt.Errorf("some items are no longer available")
    // }

    now := time.Now()
    ctx.PaidAmount = amount
    ctx.PaidAt = &now
    ctx.setState(&PaidState{})
    return nil
}

History State: Melacak Riwayat Transisi #

Menambahkan metadata ke setiap transisi membuat sistem lebih auditable dan debuggable.

// StateTransition yang lebih kaya dengan informasi kontekstual
type StateTransition struct {
    From      string
    To        string
    At        time.Time
    Actor     string // siapa yang memicu transisi (user ID, system)
    Reason    string
    Metadata  map[string]interface{}
}

// setState yang lebih informatif
func (o *Order) setStateWithAudit(newState OrderState, actor, reason string, meta map[string]interface{}) {
    transition := StateTransition{
        From:     o.state.StateName(),
        To:       newState.StateName(),
        At:       time.Now(),
        Actor:    actor,
        Reason:   reason,
        Metadata: meta,
    }
    o.history = append(o.history, transition)
    o.state = newState
}

// Penggunaan di concrete state:
func (s *PendingPaymentState) Pay(ctx *Order, amount float64) error {
    // ... validasi ...
    ctx.setStateWithAudit(&PaidState{}, "payment-gateway", "payment confirmed",
        map[string]interface{}{
            "amount":         amount,
            "payment_method": "credit_card",
        },
    )
    return nil
}

State Pattern vs Strategy Pattern #

State dan Strategy adalah dua pattern yang sering dikacaukan karena keduanya menggunakan composition untuk mengubah behavior. Perbedaan kritis ada di siapa yang mengontrol pergantian dan apakah ada relasi antar “state”.

// Strategy: client memilih algoritma dari luar
svc.SetShippingStrategy(jneStrategy)
svc.SetShippingStrategy(jtStrategy)
// Client bisa set ke algoritma manapun kapan saja; tidak ada urutan yang dipaksa

// State: objek bertransisi sendiri berdasarkan kondisi internal
order.Pay(amount)    // PendingPayment → Paid (state menentukan transisi)
order.Ship(tracking) // harus melalui Processing dulu; tidak bisa skip
// Client tidak bisa paksa transisi yang tidak valid
Aspek State Strategy
Siapa yang ganti Objek sendiri (via setState dari dalam State) Client dari luar
State tahu satu sama lain? Ya — state A tahu state B untuk transisi Tidak — setiap strategi independen
Ada urutan/alur yang dipaksa? Ya — hanya transisi yang valid diizinkan Tidak — bisa ganti ke strategi apapun
Contoh Order lifecycle, koneksi jaringan Algoritma sort, metode pengiriman

Studi Kasus Kedua: Network Connection State #

// ConnectionState mendefinisikan semua operasi pada koneksi jaringan.
type ConnectionState interface {
    Connect(ctx *Connection) error
    Disconnect(ctx *Connection) error
    Send(ctx *Connection, data []byte) error
    Receive(ctx *Connection) ([]byte, error)
    StateName() string
}

type Connection struct {
    host    string
    port    int
    state   ConnectionState
    conn    net.Conn
}

func NewConnection(host string, port int) *Connection {
    c := &Connection{host: host, port: port}
    c.state = &DisconnectedState{}
    return c
}

// DisconnectedState — belum terhubung.
// Operasi valid: Connect.
type DisconnectedState struct{}

func (s *DisconnectedState) Connect(ctx *Connection) error {
    addr := fmt.Sprintf("%s:%d", ctx.host, ctx.port)
    conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
    if err != nil {
        return fmt.Errorf("connection failed: %w", err)
    }
    ctx.conn = conn
    ctx.state = &ConnectedState{}
    return nil
}

func (s *DisconnectedState) Disconnect(ctx *Connection) error {
    return fmt.Errorf("already disconnected")
}

func (s *DisconnectedState) Send(ctx *Connection, data []byte) error {
    return fmt.Errorf("cannot send: not connected")
}

func (s *DisconnectedState) Receive(ctx *Connection) ([]byte, error) {
    return nil, fmt.Errorf("cannot receive: not connected")
}

func (s *DisconnectedState) StateName() string { return "disconnected" }


// ConnectedState — terhubung dan siap.
// Operasi valid: Send, Receive, Disconnect.
type ConnectedState struct{}

func (s *ConnectedState) Connect(ctx *Connection) error {
    return fmt.Errorf("already connected")
}

func (s *ConnectedState) Disconnect(ctx *Connection) error {
    if err := ctx.conn.Close(); err != nil {
        return fmt.Errorf("disconnect failed: %w", err)
    }
    ctx.conn = nil
    ctx.state = &DisconnectedState{}
    return nil
}

func (s *ConnectedState) Send(ctx *Connection, data []byte) error {
    _, err := ctx.conn.Write(data)
    if err != nil {
        ctx.state = &DisconnectedState{} // auto-transition jika error
        return fmt.Errorf("send failed (connection dropped): %w", err)
    }
    return nil
}

func (s *ConnectedState) Receive(ctx *Connection) ([]byte, error) {
    buf := make([]byte, 4096)
    n, err := ctx.conn.Read(buf)
    if err != nil {
        ctx.state = &DisconnectedState{}
        return nil, fmt.Errorf("receive failed (connection dropped): %w", err)
    }
    return buf[:n], nil
}

func (s *ConnectedState) StateName() string { return "connected" }

Testing State Pattern #

func TestOrder_HappyPath(t *testing.T) {
    o := NewTestOrder()

    if o.CurrentState() != "pending_payment" {
        t.Errorf("expected pending_payment, got %s", o.CurrentState())
    }

    if err := o.Pay(375000); err != nil {
        t.Fatalf("Pay failed: %v", err)
    }
    if o.CurrentState() != "paid" {
        t.Errorf("expected paid after Pay, got %s", o.CurrentState())
    }

    if err := o.StartProcessing(); err != nil {
        t.Fatalf("StartProcessing failed: %v", err)
    }

    if err := o.Ship("TRACK001", "JNE"); err != nil {
        t.Fatalf("Ship failed: %v", err)
    }

    if err := o.ConfirmDelivery(); err != nil {
        t.Fatalf("ConfirmDelivery failed: %v", err)
    }
    if o.CurrentState() != "delivered" {
        t.Errorf("expected delivered, got %s", o.CurrentState())
    }

    // Verifikasi history
    if len(o.History()) != 4 {
        t.Errorf("expected 4 transitions, got %d", len(o.History()))
    }
}

func TestOrder_InvalidTransitionReturnsError(t *testing.T) {
    o := NewTestOrder()

    // Tidak bisa kirim sebelum bayar
    err := o.Ship("TRACK001", "JNE")
    if err == nil {
        t.Error("expected error when shipping before payment")
    }

    // Tidak bisa proses sebelum bayar
    err = o.StartProcessing()
    if err == nil {
        t.Error("expected error when processing before payment")
    }

    // Bayar dulu
    _ = o.Pay(375000)

    // Tidak bisa bayar lagi
    err = o.Pay(375000)
    if err == nil {
        t.Error("expected error when paying twice")
    }
}

func TestOrder_CancelAfterPaymentNeedsRefund(t *testing.T) {
    o := NewTestOrder()
    _ = o.Pay(375000)

    if err := o.Cancel("changed my mind"); err != nil {
        t.Fatalf("Cancel failed: %v", err)
    }
    if o.CurrentState() != "cancelled" {
        t.Errorf("expected cancelled, got %s", o.CurrentState())
    }
}

func TestOrder_ReturnWindowExpired(t *testing.T) {
    o := NewTestOrder()
    _ = o.Pay(375000)
    _ = o.StartProcessing()
    _ = o.Ship("TRACK001", "JNE")
    _ = o.ConfirmDelivery()

    // Manipulasi waktu pengiriman menjadi 8 hari lalu
    eightDaysAgo := time.Now().Add(-8 * 24 * time.Hour)
    o.DeliveredAt = &eightDaysAgo

    err := o.RequestReturn("defective product")
    if err == nil {
        t.Error("expected error when return window expired")
    }
}

func TestPendingPaymentState_InsufficientPayment(t *testing.T) {
    o := NewTestOrder()
    err := o.Pay(100) // jauh di bawah total amount
    if err == nil {
        t.Error("expected error for insufficient payment")
    }
    if o.CurrentState() != "pending_payment" {
        t.Errorf("state should not change after failed payment")
    }
}

func NewTestOrder() *Order {
    return NewOrder("TEST-001", "user-1", []OrderItem{
        {ProductID: "PRD-1", Quantity: 1, Price: 375000},
    }, 375000)
}

Kapan Menggunakan dan Kapan Tidak #

GUNAKAN State Pattern jika:
  ✓ Objek memiliki behavior yang secara signifikan berbeda berdasarkan state
  ✓ Ada aturan transisi yang harus ditegakkan (tidak semua transisi valid)
  ✓ if-else atau switch berbasis status sudah terlalu besar dan duplikatif
  ✓ State baru akan sering ditambahkan tanpa mengubah Context atau state lain
  ✓ Perlu audit trail yang jelas tentang kapan dan mengapa state berubah

HINDARI State Pattern jika:
  ✗ Hanya ada 2-3 state sederhana yang tidak akan bertambah
  ✗ Behavior antar state tidak cukup berbeda untuk dipisahkan
  ✗ Semua state valid dari semua state lain (tidak ada aturan transisi)
  ✗ State tidak perlu melakukan transisi sendiri — gunakan simple enum

State Machine Library vs State Pattern

Untuk state machine yang sangat kompleks dengan banyak state dan transisi, pertimbangkan menggunakan state machine library seperti looplab/fsm yang menyediakan definisi state dan transisi secara deklaratif. State Pattern lebih cocok ketika setiap state memiliki behavior yang kompleks dan berbeda, bukan hanya label yang berbeda. Gunakan library ketika kamu butuh fitur seperti guards, callbacks, dan visualisasi state machine.


Checklist Review State Pattern #

DESAIN:
  □ Setiap state hanya mengimplementasikan method yang valid untuk state tersebut
  □ Method yang tidak valid mengembalikan error informatif (bukan panic)
  □ BaseState menghindari duplikasi implementasi "operasi tidak valid"
  □ Transisi selalu dilakukan melalui setState, tidak dengan assignment langsung

TRANSISI:
  □ State yang membuat transisi sudah diverifikasi benar (tidak bisa skip state)
  □ Guard condition divalidasi sebelum setState dipanggil
  □ Terminal state (Cancelled, Returned, Delivered) tidak bisa bertransisi
  □ History transisi dicatat dengan timestamp dan informasi yang cukup

TESTING:
  □ Happy path: semua transisi yang valid ditest secara berurutan
  □ Setiap transisi yang invalid ditest — error yang tepat dikembalikan
  □ State tidak berubah setelah operasi yang gagal
  □ Guard condition ditest (pembayaran kurang, window expired, dll)
  □ Terminal state tidak bisa ditransisi lebih lanjut

Ringkasan #

  • State Pattern mendelegasikan behavior ke objek state — Context (Order) tidak tahu aturan transisi; setiap state yang memutuskan apa yang valid dan ke state mana bisa bertransisi.
  • Transisi yang tidak valid menghasilkan error, bukan silent bug — ini adalah jaminan utama State Pattern; tidak ada cara untuk memaksa transisi yang tidak sah dari luar.
  • BaseState menghilangkan duplikasi — implementasikan sekali default “operasi tidak valid” di BaseState; concrete state hanya override method yang valid untuk state tersebut.
  • setState hanya dipanggil dari dalam State, bukan dari client — ini yang menjaga enkapsulasi; Context menyediakan setState sebagai package-level method, bukan public API.
  • Guard condition memperkuat transisi — validasi tambahan (batas waktu pembayaran, window return) diimplementasikan di dalam state sebelum memanggil setState.
  • History transisi sangat berharga — catat siapa yang memicu, kapan, dan mengapa; ini sangat membantu debugging dan audit.
  • Bedakan dari Strategy: State bertransisi sendiri dan state saling tahu satu sama lain; Strategy dipilih dari luar dan setiap strategi independen.
  • Untuk state machine yang kompleks, pertimbangkan library seperti looplab/fsm — State Pattern lebih tepat ketika behavior per state sangat berbeda dan kaya.

← Sebelumnya: Chain of Responsibility   Berikutnya: Template Method →

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