Command Pattern #

Sebuah text editor membutuhkan fitur undo. Setiap kali pengguna mengetik huruf, menghapus kata, atau memformat teks, aksi tersebut harus bisa dibatalkan dan diulang kembali. Masalahnya: bagaimana menyimpan “jejak” setiap aksi agar bisa dibalik? Kamu tidak bisa hanya menyimpan string “ketik huruf A” — kamu perlu menyimpan semua informasi yang cukup untuk membalik aksi itu: di posisi mana, apa yang ada sebelumnya, apa yang berubah. Command Pattern mengubah setiap aksi menjadi objek — objek yang membawa semua informasi yang diperlukan untuk mengeksekusi, membatalkan, dan bahkan mengulang aksi tersebut. Objek command bisa disimpan dalam stack, dimasukkan ke queue, dikirim lewat network, atau dijadwalkan untuk nanti. Kode yang meminta eksekusi tidak perlu tahu bagaimana aksi dilakukan — ia hanya tahu bahwa ada objek command yang bisa di-Execute().

Apa itu Command Pattern? #

Command Pattern adalah behavioral design pattern yang mengubah request menjadi objek mandiri yang menyimpan semua informasi yang dibutuhkan untuk melaksanakan request tersebut — termasuk receiver yang akan melakukannya, method yang harus dipanggil, dan parameter yang diperlukan. Request yang sudah berbentuk objek ini bisa disimpan, diqueue, dikirim, di-log, atau dibalik.

Transformasi “request → objek” adalah kunci. Ini membuka empat kemampuan yang tidak mungkin dilakukan dengan pemanggilan fungsi biasa:

  • Deferred execution — simpan command, eksekusi nanti
  • Undo/Redo — setiap command tahu cara membalik dirinya sendiri
  • Queue dan scheduling — command bisa diqueue untuk dieksekusi secara berurutan atau terjadwal
  • Macro command — gabungkan beberapa command menjadi satu command yang lebih besar
flowchart LR
    subgraph "Tanpa Command"
        C1[Client] -->|"editor.insertChar('A', pos)"| R1[TextEditor]
        C1 -->|"editor.deleteChar(pos)"| R1
        C1 -->|"editor.formatBold(range)"| R1
        note1["Tidak ada jejak,\ntidak ada undo"]
    end

    subgraph "Dengan Command"
        C2[Client] -->|"Execute()"| CMD[InsertCharCommand\n- char: 'A'\n- pos: 5\n- prevState: ..."]
        CMD -->|"receiver.Insert()"| R2[TextEditor]
        CMD -->|"disimpan di history"| H[UndoStack]
        note2["Bisa undo, redo,\nbisa di-replay"]
    end

Empat Komponen Command Pattern #

Command Pattern melibatkan empat komponen dengan peran yang berbeda tapi saling melengkapi.

classDiagram
    class Command {
        <<interface>>
        +Execute() error
        +Undo() error
        +Description() string
    }

    class TextEditor {
        -content []rune
        -cursor int
        +Insert(pos int, char rune)
        +Delete(pos int) rune
        +GetContent() string
    }

    class InsertCommand {
        -editor TextEditor
        -char rune
        -position int
        +Execute() error
        +Undo() error
        +Description() string
    }

    class DeleteCommand {
        -editor TextEditor
        -position int
        -deletedChar rune
        +Execute() error
        +Undo() error
        +Description() string
    }

    class CommandHistory {
        -undoStack []Command
        -redoStack []Command
        +Execute(cmd Command) error
        +Undo() error
        +Redo() error
    }

    Command <|.. InsertCommand
    Command <|.. DeleteCommand
    InsertCommand --> TextEditor : receiver
    DeleteCommand --> TextEditor : receiver
    CommandHistory o-- Command : manages history
Komponen Peran Analogi
Command interface Kontrak untuk semua command: Execute, Undo Lembar instruksi standar
Concrete Command Menyimpan receiver + parameter; tahu cara Execute dan Undo Instruksi spesifik yang sudah terisi
Receiver Objek yang benar-benar melakukan pekerjaan Orang yang mengerjakan instruksi
Invoker Menyimpan dan mengeksekusi command; tidak tahu detailnya Manager yang meneruskan instruksi

Implementasi Lengkap: Text Editor dengan Undo/Redo #

Receiver: TextEditor #

package editor

import (
    "fmt"
    "strings"
)

// TextEditor adalah Receiver — komponen yang benar-benar mengeksekusi operasi teks.
// Tidak ada logic undo di sini; undo adalah tanggung jawab Command.
type TextEditor struct {
    content []rune
    cursor  int
}

func NewTextEditor(initialContent string) *TextEditor {
    return &TextEditor{
        content: []rune(initialContent),
        cursor:  len([]rune(initialContent)),
    }
}

// Insert menyisipkan rune pada posisi tertentu.
func (e *TextEditor) Insert(pos int, char rune) error {
    if pos < 0 || pos > len(e.content) {
        return fmt.Errorf("invalid position %d (content length: %d)", pos, len(e.content))
    }
    e.content = append(e.content[:pos], append([]rune{char}, e.content[pos:]...)...)
    if e.cursor >= pos {
        e.cursor++
    }
    return nil
}

// Delete menghapus rune pada posisi tertentu dan mengembalikan rune yang dihapus.
func (e *TextEditor) Delete(pos int) (rune, error) {
    if pos < 0 || pos >= len(e.content) {
        return 0, fmt.Errorf("invalid position %d (content length: %d)", pos, len(e.content))
    }
    deleted := e.content[pos]
    e.content = append(e.content[:pos], e.content[pos+1:]...)
    if e.cursor > pos {
        e.cursor--
    }
    return deleted, nil
}

// Replace mengganti teks dalam rentang tertentu.
func (e *TextEditor) Replace(start, end int, newText string) (string, error) {
    if start < 0 || end > len(e.content) || start > end {
        return "", fmt.Errorf("invalid range [%d, %d]", start, end)
    }
    oldText := string(e.content[start:end])
    newRunes := []rune(newText)
    e.content = append(e.content[:start], append(newRunes, e.content[end:]...)...)
    return oldText, nil
}

// GetContent mengembalikan isi dokumen saat ini.
func (e *TextEditor) GetContent() string { return string(e.content) }

// GetCursor mengembalikan posisi cursor saat ini.
func (e *TextEditor) GetCursor() int { return e.cursor }

// SetCursor memindahkan cursor ke posisi tertentu.
func (e *TextEditor) SetCursor(pos int) { e.cursor = pos }

Command Interface #

package editor

// Command adalah interface untuk semua operasi editor.
// Setiap Command harus bisa di-Execute dan di-Undo.
type Command interface {
    // Execute menjalankan operasi.
    Execute() error

    // Undo membalik operasi yang sudah dieksekusi.
    Undo() error

    // Description mengembalikan deskripsi singkat untuk logging dan UI.
    Description() string
}

Concrete Commands #

package editor

import "fmt"

// InsertCommand menyisipkan satu karakter ke dalam dokumen.
type InsertCommand struct {
    editor   *TextEditor
    char     rune
    position int
    executed bool
}

func NewInsertCommand(editor *TextEditor, char rune, position int) Command {
    return &InsertCommand{editor: editor, char: char, position: position}
}

func (c *InsertCommand) Execute() error {
    if err := c.editor.Insert(c.position, c.char); err != nil {
        return fmt.Errorf("insert failed: %w", err)
    }
    c.executed = true
    return nil
}

func (c *InsertCommand) Undo() error {
    if !c.executed {
        return fmt.Errorf("cannot undo: command has not been executed")
    }
    if _, err := c.editor.Delete(c.position); err != nil {
        return fmt.Errorf("undo insert failed: %w", err)
    }
    c.executed = false
    return nil
}

func (c *InsertCommand) Description() string {
    return fmt.Sprintf("Insert '%c' at position %d", c.char, c.position)
}


// DeleteCommand menghapus satu karakter dari dokumen.
type DeleteCommand struct {
    editor      *TextEditor
    position    int
    deletedChar rune  // disimpan saat Execute untuk keperluan Undo
    executed    bool
}

func NewDeleteCommand(editor *TextEditor, position int) Command {
    return &DeleteCommand{editor: editor, position: position}
}

func (c *DeleteCommand) Execute() error {
    deleted, err := c.editor.Delete(c.position)
    if err != nil {
        return fmt.Errorf("delete failed: %w", err)
    }
    c.deletedChar = deleted
    c.executed = true
    return nil
}

func (c *DeleteCommand) Undo() error {
    if !c.executed {
        return fmt.Errorf("cannot undo: command has not been executed")
    }
    if err := c.editor.Insert(c.position, c.deletedChar); err != nil {
        return fmt.Errorf("undo delete failed: %w", err)
    }
    c.executed = false
    return nil
}

func (c *DeleteCommand) Description() string {
    if c.executed {
        return fmt.Sprintf("Delete '%c' at position %d", c.deletedChar, c.position)
    }
    return fmt.Sprintf("Delete at position %d", c.position)
}


// ReplaceCommand mengganti teks dalam rentang tertentu.
type ReplaceCommand struct {
    editor   *TextEditor
    start    int
    end      int
    newText  string
    oldText  string // disimpan saat Execute untuk keperluan Undo
    executed bool
}

func NewReplaceCommand(editor *TextEditor, start, end int, newText string) Command {
    return &ReplaceCommand{editor: editor, start: start, end: end, newText: newText}
}

func (c *ReplaceCommand) Execute() error {
    oldText, err := c.editor.Replace(c.start, c.end, c.newText)
    if err != nil {
        return fmt.Errorf("replace failed: %w", err)
    }
    c.oldText = oldText
    c.executed = true
    return nil
}

func (c *ReplaceCommand) Undo() error {
    if !c.executed {
        return fmt.Errorf("cannot undo: command has not been executed")
    }
    // Hitung end posisi yang baru setelah newText dimasukkan
    newEnd := c.start + len([]rune(c.newText))
    if _, err := c.editor.Replace(c.start, newEnd, c.oldText); err != nil {
        return fmt.Errorf("undo replace failed: %w", err)
    }
    c.executed = false
    return nil
}

func (c *ReplaceCommand) Description() string {
    return fmt.Sprintf("Replace [%d:%d] with %q", c.start, c.end, c.newText)
}

Macro Command: Gabungkan Beberapa Command #

// MacroCommand menggabungkan beberapa command menjadi satu unit yang bisa di-Execute dan di-Undo.
// Berguna untuk operasi yang terdiri dari beberapa langkah yang harus dianggap atomik.
type MacroCommand struct {
    commands    []Command
    description string
    executed    int // jumlah command yang sudah berhasil dieksekusi
}

func NewMacroCommand(description string, commands ...Command) Command {
    return &MacroCommand{
        commands:    commands,
        description: description,
    }
}

func (m *MacroCommand) Execute() error {
    for i, cmd := range m.commands {
        if err := cmd.Execute(); err != nil {
            // Rollback semua yang sudah berhasil
            for j := i - 1; j >= 0; j-- {
                _ = m.commands[j].Undo()
            }
            return fmt.Errorf("macro failed at step %d (%s): %w", i+1, cmd.Description(), err)
        }
        m.executed++
    }
    return nil
}

func (m *MacroCommand) Undo() error {
    // Undo dalam urutan terbalik
    for i := m.executed - 1; i >= 0; i-- {
        if err := m.commands[i].Undo(); err != nil {
            return fmt.Errorf("macro undo failed at step %d: %w", i+1, err)
        }
    }
    m.executed = 0
    return nil
}

func (m *MacroCommand) Description() string { return m.description }

Invoker: CommandHistory dengan Undo/Redo #

// CommandHistory adalah Invoker — menyimpan history command dan mengorkestrasikan undo/redo.
type CommandHistory struct {
    undoStack []Command
    redoStack []Command
    maxSize   int
}

func NewCommandHistory(maxSize int) *CommandHistory {
    return &CommandHistory{
        undoStack: make([]Command, 0, maxSize),
        redoStack: make([]Command, 0, maxSize),
        maxSize:   maxSize,
    }
}

// Execute menjalankan command dan menyimpannya ke undo stack.
// Setiap Execute baru mengosongkan redo stack.
func (h *CommandHistory) Execute(cmd Command) error {
    if err := cmd.Execute(); err != nil {
        return err
    }

    // Tambahkan ke undo stack
    if len(h.undoStack) >= h.maxSize {
        // Hapus command tertua jika sudah melebihi batas
        h.undoStack = h.undoStack[1:]
    }
    h.undoStack = append(h.undoStack, cmd)

    // Kosongkan redo stack — aksi baru membatalkan semua "redo yang tersedia"
    h.redoStack = h.redoStack[:0]

    return nil
}

// Undo membatalkan command terakhir yang dieksekusi.
func (h *CommandHistory) Undo() error {
    if len(h.undoStack) == 0 {
        return fmt.Errorf("nothing to undo")
    }

    // Pop dari undo stack
    lastIdx := len(h.undoStack) - 1
    cmd := h.undoStack[lastIdx]
    h.undoStack = h.undoStack[:lastIdx]

    if err := cmd.Undo(); err != nil {
        // Kembalikan ke undo stack jika gagal
        h.undoStack = append(h.undoStack, cmd)
        return fmt.Errorf("undo failed: %w", err)
    }

    // Pindahkan ke redo stack
    h.redoStack = append(h.redoStack, cmd)
    return nil
}

// Redo mengulang command yang terakhir di-undo.
func (h *CommandHistory) Redo() error {
    if len(h.redoStack) == 0 {
        return fmt.Errorf("nothing to redo")
    }

    // Pop dari redo stack
    lastIdx := len(h.redoStack) - 1
    cmd := h.redoStack[lastIdx]
    h.redoStack = h.redoStack[:lastIdx]

    if err := cmd.Execute(); err != nil {
        h.redoStack = append(h.redoStack, cmd)
        return fmt.Errorf("redo failed: %w", err)
    }

    h.undoStack = append(h.undoStack, cmd)
    return nil
}

// CanUndo memeriksa apakah ada command yang bisa di-undo.
func (h *CommandHistory) CanUndo() bool { return len(h.undoStack) > 0 }

// CanRedo memeriksa apakah ada command yang bisa di-redo.
func (h *CommandHistory) CanRedo() bool { return len(h.redoStack) > 0 }

// UndoCount mengembalikan jumlah command yang bisa di-undo.
func (h *CommandHistory) UndoCount() int { return len(h.undoStack) }

// History mengembalikan deskripsi semua command dalam undo stack (untuk UI).
func (h *CommandHistory) History() []string {
    result := make([]string, len(h.undoStack))
    for i, cmd := range h.undoStack {
        result[i] = cmd.Description()
    }
    return result
}

Demonstrasi Penggunaan #

func main() {
    editor := editor.NewTextEditor("Hello")
    history := editor.NewCommandHistory(50)

    fmt.Printf("Initial: %q\n", editor.GetContent())

    // Eksekusi beberapa command
    _ = history.Execute(editor.NewInsertCommand(editor, ' ', 5))
    _ = history.Execute(editor.NewInsertCommand(editor, 'W', 6))
    _ = history.Execute(editor.NewInsertCommand(editor, 'o', 7))
    _ = history.Execute(editor.NewInsertCommand(editor, 'r', 8))
    _ = history.Execute(editor.NewInsertCommand(editor, 'l', 9))
    _ = history.Execute(editor.NewInsertCommand(editor, 'd', 10))

    fmt.Printf("After typing: %q\n", editor.GetContent()) // "Hello World"

    // Undo 3 kali
    _ = history.Undo()
    _ = history.Undo()
    _ = history.Undo()
    fmt.Printf("After 3 undos: %q\n", editor.GetContent()) // "Hello Wor"

    // Redo 1 kali
    _ = history.Redo()
    fmt.Printf("After 1 redo: %q\n", editor.GetContent()) // "Hello Worl"

    // Macro command: ganti kata "Hello" dengan "Hi"
    findReplace := editor.NewMacroCommand("Replace Hello with Hi",
        editor.NewDeleteCommand(editor, 4), // hapus 'o'
        editor.NewDeleteCommand(editor, 3), // hapus 'l'
        editor.NewDeleteCommand(editor, 2), // hapus 'l'
        editor.NewDeleteCommand(editor, 1), // hapus 'e'
        editor.NewInsertCommand(editor, 'i', 1), // sisipkan 'i'
    )
    _ = history.Execute(findReplace)
    fmt.Printf("After macro: %q\n", editor.GetContent()) // "Hi Worl"

    // Undo seluruh macro sekaligus
    _ = history.Undo()
    fmt.Printf("After undo macro: %q\n", editor.GetContent()) // "Hello Worl"

    fmt.Printf("History (%d commands):\n", history.UndoCount())
    for i, h := range history.History() {
        fmt.Printf("  %d. %s\n", i+1, h)
    }
}

Studi Kasus Kedua: Job Queue System #

Command Pattern adalah fondasi dari job queue — setiap job adalah command yang bisa disimpan, diqueue, dan dieksekusi oleh worker pool.

package jobqueue

import (
    "context"
    "fmt"
    "log/slog"
    "sync"
    "time"
)

// Job adalah Command interface untuk sistem antrian pekerjaan.
type Job interface {
    Execute(ctx context.Context) error
    JobID() string
    JobType() string
    MaxRetries() int
}

// JobQueue adalah Invoker yang mengelola antrian dan eksekusi job.
type JobQueue struct {
    queue   chan Job
    workers int
    logger  *slog.Logger
    wg      sync.WaitGroup
    ctx     context.Context
    cancel  context.CancelFunc
}

func NewJobQueue(workers, bufferSize int, logger *slog.Logger) *JobQueue {
    ctx, cancel := context.WithCancel(context.Background())
    return &JobQueue{
        queue:   make(chan Job, bufferSize),
        workers: workers,
        logger:  logger,
        ctx:     ctx,
        cancel:  cancel,
    }
}

// Start memulai worker pool.
func (q *JobQueue) Start() {
    for i := 0; i < q.workers; i++ {
        q.wg.Add(1)
        go q.worker(i)
    }
}

// Enqueue memasukkan job ke antrian.
func (q *JobQueue) Enqueue(job Job) error {
    select {
    case q.queue <- job:
        q.logger.Info("job enqueued", "job_id", job.JobID(), "type", job.JobType())
        return nil
    case <-q.ctx.Done():
        return fmt.Errorf("queue is shutting down")
    default:
        return fmt.Errorf("queue is full")
    }
}

// Stop menghentikan queue secara graceful.
func (q *JobQueue) Stop() {
    q.cancel()
    close(q.queue)
    q.wg.Wait()
}

func (q *JobQueue) worker(id int) {
    defer q.wg.Done()
    for job := range q.queue {
        q.executeWithRetry(job)
    }
}

func (q *JobQueue) executeWithRetry(job Job) {
    var lastErr error
    for attempt := 0; attempt <= job.MaxRetries(); attempt++ {
        if attempt > 0 {
            time.Sleep(time.Duration(attempt) * time.Second)
            q.logger.Info("retrying job", "job_id", job.JobID(), "attempt", attempt+1)
        }

        if err := job.Execute(q.ctx); err != nil {
            lastErr = err
            q.logger.Warn("job failed", "job_id", job.JobID(), "attempt", attempt+1, "error", err)
            continue
        }

        q.logger.Info("job completed", "job_id", job.JobID())
        return
    }

    q.logger.Error("job exhausted retries",
        "job_id", job.JobID(),
        "max_retries", job.MaxRetries(),
        "last_error", lastErr,
    )
}

Concrete Job — setiap job type adalah command konkret:

// SendEmailJob adalah Command konkret untuk mengirim email.
type SendEmailJob struct {
    id         string
    recipient  string
    subject    string
    body       string
    emailSvc   EmailService
}

func NewSendEmailJob(id, recipient, subject, body string, svc EmailService) Job {
    return &SendEmailJob{
        id: id, recipient: recipient,
        subject: subject, body: body, emailSvc: svc,
    }
}

func (j *SendEmailJob) Execute(ctx context.Context) error {
    return j.emailSvc.Send(ctx, j.recipient, j.subject, j.body)
}

func (j *SendEmailJob) JobID() string    { return j.id }
func (j *SendEmailJob) JobType() string  { return "send_email" }
func (j *SendEmailJob) MaxRetries() int  { return 3 }


// GenerateReportJob adalah Command konkret untuk generate laporan.
type GenerateReportJob struct {
    id        string
    reportType string
    period    string
    outputPath string
    reportSvc  ReportService
}

func NewGenerateReportJob(id, reportType, period, outputPath string, svc ReportService) Job {
    return &GenerateReportJob{
        id: id, reportType: reportType,
        period: period, outputPath: outputPath, reportSvc: svc,
    }
}

func (j *GenerateReportJob) Execute(ctx context.Context) error {
    return j.reportSvc.Generate(ctx, j.reportType, j.period, j.outputPath)
}

func (j *GenerateReportJob) JobID() string    { return j.id }
func (j *GenerateReportJob) JobType() string  { return "generate_report" }
func (j *GenerateReportJob) MaxRetries() int  { return 1 }

// Penggunaan
func setupJobQueue() {
    queue := jobqueue.NewJobQueue(5, 100, slog.Default())
    queue.Start()

    // Enqueue berbagai jenis job
    _ = queue.Enqueue(NewSendEmailJob("email-001", "[email protected]",
        "Order Confirmed", "Your order has been confirmed!", emailSvc))
    _ = queue.Enqueue(NewGenerateReportJob("report-001",
        "financial", "2024-Q1", "/reports/q1.pdf", reportSvc))
    _ = queue.Enqueue(NewSendEmailJob("email-002", "[email protected]",
        "Daily Summary", "Here is your daily summary...", emailSvc))

    // Queue menjalankan semua job secara concurrent dengan retry
    time.Sleep(10 * time.Second)
    queue.Stop()
}

Functional Command: Idiom Golang #

Untuk kasus yang lebih sederhana tanpa kebutuhan Undo, Golang memungkinkan implementasi Command yang sangat ringkas menggunakan function type.

// SimpleCommand adalah function type — lebih ringkas dari struct untuk command tanpa Undo
type SimpleCommand func(ctx context.Context) error

// CommandQueue mengeksekusi SimpleCommand secara berurutan.
type CommandQueue struct {
    commands []SimpleCommand
}

func (q *CommandQueue) Add(cmd SimpleCommand) {
    q.commands = append(q.commands, cmd)
}

func (q *CommandQueue) ExecuteAll(ctx context.Context) error {
    for i, cmd := range q.commands {
        if err := cmd(ctx); err != nil {
            return fmt.Errorf("command %d failed: %w", i+1, err)
        }
    }
    return nil
}

// Penggunaan — command sebagai closure
queue := &CommandQueue{}

queue.Add(func(ctx context.Context) error {
    return userSvc.DeductBalance(ctx, userID, amount)
})

queue.Add(func(ctx context.Context) error {
    return inventorySvc.ReserveStock(ctx, productID, qty)
})

queue.Add(func(ctx context.Context) error {
    return notifSvc.SendConfirmation(ctx, userEmail)
})

if err := queue.ExecuteAll(ctx); err != nil {
    log.Printf("pipeline failed: %v", err)
}

Alur Undo/Redo yang Benar #

sequenceDiagram
    participant U as User
    participant H as CommandHistory
    participant C as Command
    participant E as TextEditor

    U->>H: Execute(InsertCommand 'A')
    H->>C: Execute()
    C->>E: Insert('A', pos)
    H->>H: push ke undoStack
    H->>H: clear redoStack

    U->>H: Execute(InsertCommand 'B')
    H->>C: Execute()
    C->>E: Insert('B', pos)
    H->>H: push ke undoStack

    U->>H: Undo()
    H->>H: pop InsertCommand 'B' dari undoStack
    H->>C: Undo()
    C->>E: Delete(pos of 'B')
    H->>H: push ke redoStack

    U->>H: Redo()
    H->>H: pop InsertCommand 'B' dari redoStack
    H->>C: Execute()
    C->>E: Insert('B', pos)
    H->>H: push ke undoStack

    U->>H: Execute(NewCommand)
    H->>H: push ke undoStack
    H->>H: clear redoStack ← redo stack dikosongkan!

Testing Command Pattern #

func TestInsertCommand_ExecuteAndUndo(t *testing.T) {
    ed := NewTextEditor("Hello")
    cmd := NewInsertCommand(ed, ' ', 5)

    if err := cmd.Execute(); err != nil {
        t.Fatalf("Execute failed: %v", err)
    }
    if ed.GetContent() != "Hello " {
        t.Errorf("after execute: expected 'Hello ', got %q", ed.GetContent())
    }

    if err := cmd.Undo(); err != nil {
        t.Fatalf("Undo failed: %v", err)
    }
    if ed.GetContent() != "Hello" {
        t.Errorf("after undo: expected 'Hello', got %q", ed.GetContent())
    }
}

func TestDeleteCommand_StoresDeletedChar(t *testing.T) {
    ed := NewTextEditor("Hello")
    cmd := NewDeleteCommand(ed, 4) // hapus 'o'

    if err := cmd.Execute(); err != nil {
        t.Fatalf("Execute failed: %v", err)
    }
    if ed.GetContent() != "Hell" {
        t.Errorf("after delete: expected 'Hell', got %q", ed.GetContent())
    }

    if err := cmd.Undo(); err != nil {
        t.Fatalf("Undo failed: %v", err)
    }
    if ed.GetContent() != "Hello" {
        t.Errorf("after undo delete: expected 'Hello', got %q", ed.GetContent())
    }
}

func TestCommandHistory_UndoRedo(t *testing.T) {
    ed := NewTextEditor("")
    history := NewCommandHistory(10)

    _ = history.Execute(NewInsertCommand(ed, 'A', 0))
    _ = history.Execute(NewInsertCommand(ed, 'B', 1))
    _ = history.Execute(NewInsertCommand(ed, 'C', 2))

    if ed.GetContent() != "ABC" {
        t.Errorf("expected 'ABC', got %q", ed.GetContent())
    }

    _ = history.Undo()
    if ed.GetContent() != "AB" {
        t.Errorf("after undo: expected 'AB', got %q", ed.GetContent())
    }

    _ = history.Undo()
    if ed.GetContent() != "A" {
        t.Errorf("after 2 undos: expected 'A', got %q", ed.GetContent())
    }

    _ = history.Redo()
    if ed.GetContent() != "AB" {
        t.Errorf("after redo: expected 'AB', got %q", ed.GetContent())
    }
}

func TestCommandHistory_NewExecuteClearsRedoStack(t *testing.T) {
    ed := NewTextEditor("AB")
    history := NewCommandHistory(10)

    _ = history.Execute(NewInsertCommand(ed, 'C', 2))
    _ = history.Undo()

    if !history.CanRedo() {
        t.Error("expected redo available after undo")
    }

    // Execute command baru — redo stack harus dikosongkan
    _ = history.Execute(NewInsertCommand(ed, 'D', 2))
    if history.CanRedo() {
        t.Error("expected redo stack cleared after new execute")
    }
}

func TestMacroCommand_RollbackOnFailure(t *testing.T) {
    ed := NewTextEditor("Hello")
    history := NewCommandHistory(10)

    // Macro dengan command yang akan gagal (posisi invalid)
    macro := NewMacroCommand("Bad Macro",
        NewInsertCommand(ed, 'X', 5),
        NewInsertCommand(ed, 'Y', 999), // invalid position — akan gagal
    )

    err := history.Execute(macro)
    if err == nil {
        t.Error("expected error from macro with invalid command")
    }

    // Rollback harus terjadi — konten harus kembali ke "Hello"
    if ed.GetContent() != "Hello" {
        t.Errorf("expected rollback to 'Hello', got %q", ed.GetContent())
    }
}

Kapan Menggunakan dan Kapan Tidak #

GUNAKAN Command jika:
  ✓ Perlu undo/redo — ini adalah use case utama Command Pattern
  ✓ Perlu menyimpan history operasi untuk audit atau replay
  ✓ Perlu menjalankan operasi secara asinkron atau terjadwal
  ✓ Ingin menggabungkan beberapa operasi menjadi satu unit (macro)
  ✓ Membangun sistem job queue atau task scheduler

HINDARI Command jika:
  ✗ Tidak ada kebutuhan undo, queue, atau scheduling — function biasa sudah cukup
  ✗ Command tidak membawa state apapun — pertimbangkan Strategy Pattern
  ✗ Overhead struct terlalu besar untuk operasi yang sangat sederhana
  ✗ Semua command perlu tahu satu sama lain — pertimbangkan Mediator

Command di Framework Populer

database/sql menggunakan konsep Command melalui sql.Tx — setiap query dalam transaksi adalah command yang bisa di-rollback. HTTP request handler bisa dianggap Command dalam konteks web framework. context.WithCancel memungkinkan pembatalan command yang sedang berjalan. Memahami Command Pattern membantu kamu membaca dan memahami pola-pola ini lebih dalam.


Checklist Review Command #

DESAIN:
  □ Setiap Command hanya bertanggung jawab atas satu aksi yang jelas
  □ Command menyimpan semua state yang dibutuhkan untuk Execute dan Undo
  □ Invoker tidak tahu detail implementasi command — hanya memanggil Execute/Undo
  □ Receiver (objek yang melakukan pekerjaan) terpisah dari Command

UNDO/REDO:
  □ Execute menyimpan state yang diperlukan untuk Undo (misal: karakter yang dihapus)
  □ Undo mengembalikan state persis ke kondisi sebelum Execute
  □ Undo tidak dipanggil pada command yang belum di-Execute
  □ Redo stack dikosongkan ketika ada Execute baru

MACRO COMMAND:
  □ Macro rollback semua command yang sudah berhasil jika salah satu gagal
  □ Rollback terjadi dalam urutan terbalik dari eksekusi
  □ Macro memiliki deskripsi yang jelas untuk logging

TESTING:
  □ Execute dan Undo di-test secara berpasangan
  □ State setelah Undo sama persis dengan state sebelum Execute
  □ Macro rollback di-test (satu command gagal → semua di-rollback)
  □ CommandHistory: undo stack dan redo stack di-test perilakunya

Ringkasan #

  • Command mengubah request menjadi objek — bukan sekadar memanggil fungsi; request menjadi entitas yang bisa disimpan, diqueue, dikirim, dan dibalik.
  • Empat komponen: Command interface, Concrete Command (menyimpan receiver + state), Receiver (melakukan pekerjaan), dan Invoker (mengelola dan mengeksekusi command).
  • Undo adalah kekuatan utama — setiap Command menyimpan state yang cukup untuk membalik aksinya; Invoker mengelola undo/redo stack.
  • Redo stack harus dikosongkan ketika Execute baru dipanggil — ini adalah aturan undo/redo yang sering terlewat.
  • Macro Command menggabungkan beberapa command menjadi satu unit atomik; jika satu langkah gagal, semua langkah sebelumnya di-rollback.
  • Job Queue = Command Pattern — setiap job adalah Command yang diqueue oleh Invoker (queue) dan dieksekusi oleh worker (receiver); retry logic ada di Invoker.
  • Functional Command untuk kasus sederhana — function type sebagai Command jauh lebih ringkas untuk skenario tanpa Undo; gunakan struct hanya jika perlu state dan Undo.
  • Bedakan dari Strategy: Strategy mendefinisikan bagaimana sesuatu dilakukan (algoritma yang bisa ditukar); Command mendefinisikan apa yang dilakukan (request yang bisa disimpan dan dibalik).

← Sebelumnya: Observer   Berikutnya: Chain of Responsibility →

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