What is Command Pattern?

The Command pattern is a behavioral design pattern that turns a request into a stand-alone object containing all information about the request. This transformation allows you to parameterize methods with different requests, delay or queue a request’s execution, and support undoable operations. Think of it like a remote control - each button is a command that encapsulates the action to be performed on a device.

I’ll demonstrate how this pattern can help you build flexible, undoable, and queueable operations in Go applications.

Let’s start with a scenario: Text Editor Operations

Imagine you’re building a text editor that needs to support various operations like typing, deleting, copying, pasting, and most importantly, undo/redo functionality. You want to be able to execute operations, queue them, and reverse them without tightly coupling the UI to the text manipulation logic.

Without Command Pattern

Here’s how you might handle this without the Command pattern:

type TextEditor struct {
    content string
    cursor  int
    history []string // Simple history tracking
}

func (e *TextEditor) Type(text string) {
    // Save state for undo
    e.history = append(e.history, e.content)
    
    // Perform operation
    before := e.content[:e.cursor]
    after := e.content[e.cursor:]
    e.content = before + text + after
    e.cursor += len(text)
}

func (e *TextEditor) Delete(count int) {
    // Save state for undo
    e.history = append(e.history, e.content)
    
    // Perform operation
    if e.cursor >= count {
        before := e.content[:e.cursor-count]
        after := e.content[e.cursor:]
        e.content = before + after
        e.cursor -= count
    }
}

func (e *TextEditor) Undo() {
    if len(e.history) > 0 {
        e.content = e.history[len(e.history)-1]
        e.history = e.history[:len(e.history)-1]
        // But how do we restore cursor position?
        // How do we handle complex operations?
        // This approach becomes unwieldy quickly!
    }
}

// This approach has several problems:
// 1. Undo logic is mixed with operation logic
// 2. Hard to implement redo functionality
// 3. Difficult to queue or batch operations
// 4. No way to parameterize operations

With Command Pattern

Let’s refactor this using the Command pattern:

package main

import (
    "fmt"
    "log"
    "strings"
)

// Command interface
type Command interface {
    Execute() error
    Undo() error
    GetDescription() string
}

// Receiver - the object that performs the actual work
type TextEditor struct {
    content string
    cursor  int
}

func NewTextEditor() *TextEditor {
    return &TextEditor{
        content: "",
        cursor:  0,
    }
}

func (e *TextEditor) GetContent() string {
    return e.content
}

func (e *TextEditor) GetCursor() int {
    return e.cursor
}

func (e *TextEditor) SetContent(content string) {
    e.content = content
}

func (e *TextEditor) SetCursor(position int) {
    if position < 0 {
        position = 0
    }
    if position > len(e.content) {
        position = len(e.content)
    }
    e.cursor = position
}

func (e *TextEditor) InsertAt(position int, text string) {
    if position < 0 || position > len(e.content) {
        return
    }
    
    before := e.content[:position]
    after := e.content[position:]
    e.content = before + text + after
}

func (e *TextEditor) DeleteAt(position, count int) string {
    if position < 0 || position >= len(e.content) || count <= 0 {
        return ""
    }
    
    end := position + count
    if end > len(e.content) {
        end = len(e.content)
    }
    
    deleted := e.content[position:end]
    e.content = e.content[:position] + e.content[end:]
    return deleted
}

// Concrete Commands
type TypeCommand struct {
    editor       *TextEditor
    text         string
    position     int
    originalText string
}

func NewTypeCommand(editor *TextEditor, text string) *TypeCommand {
    return &TypeCommand{
        editor:   editor,
        text:     text,
        position: editor.GetCursor(),
    }
}

func (c *TypeCommand) Execute() error {
    c.position = c.editor.GetCursor()
    c.editor.InsertAt(c.position, c.text)
    c.editor.SetCursor(c.position + len(c.text))
    log.Printf("Executed: Type '%s' at position %d", c.text, c.position)
    return nil
}

func (c *TypeCommand) Undo() error {
    c.editor.DeleteAt(c.position, len(c.text))
    c.editor.SetCursor(c.position)
    log.Printf("Undone: Type '%s' at position %d", c.text, c.position)
    return nil
}

func (c *TypeCommand) GetDescription() string {
    return fmt.Sprintf("Type '%s'", c.text)
}

type DeleteCommand struct {
    editor      *TextEditor
    position    int
    count       int
    deletedText string
}

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

func (c *DeleteCommand) Execute() error {
    c.position = c.editor.GetCursor()
    
    // Delete backwards from cursor
    deleteStart := c.position - c.count
    if deleteStart < 0 {
        c.count = c.position
        deleteStart = 0
    }
    
    c.deletedText = c.editor.DeleteAt(deleteStart, c.count)
    c.editor.SetCursor(deleteStart)
    
    log.Printf("Executed: Delete %d characters at position %d", c.count, deleteStart)
    return nil
}

func (c *DeleteCommand) Undo() error {
    deleteStart := c.position - c.count
    if deleteStart < 0 {
        deleteStart = 0
    }
    
    c.editor.InsertAt(deleteStart, c.deletedText)
    c.editor.SetCursor(c.position)
    
    log.Printf("Undone: Delete %d characters, restored '%s'", c.count, c.deletedText)
    return nil
}

func (c *DeleteCommand) GetDescription() string {
    return fmt.Sprintf("Delete %d characters", c.count)
}

type CopyCommand struct {
    editor    *TextEditor
    start     int
    end       int
    clipboard *string
}

func NewCopyCommand(editor *TextEditor, start, end int, clipboard *string) *CopyCommand {
    return &CopyCommand{
        editor:    editor,
        start:     start,
        end:       end,
        clipboard: clipboard,
    }
}

func (c *CopyCommand) Execute() error {
    if c.start < 0 || c.end > len(c.editor.GetContent()) || c.start >= c.end {
        return fmt.Errorf("invalid copy range")
    }
    
    content := c.editor.GetContent()
    *c.clipboard = content[c.start:c.end]
    
    log.Printf("Executed: Copy text '%s' from position %d to %d", *c.clipboard, c.start, c.end)
    return nil
}

func (c *CopyCommand) Undo() error {
    // Copy operations are typically not undoable in the traditional sense
    // But we could clear the clipboard if needed
    log.Printf("Undone: Copy operation (clipboard cleared)")
    *c.clipboard = ""
    return nil
}

func (c *CopyCommand) GetDescription() string {
    return fmt.Sprintf("Copy text from %d to %d", c.start, c.end)
}

type PasteCommand struct {
    editor      *TextEditor
    position    int
    text        string
    originalPos int
}

func NewPasteCommand(editor *TextEditor, text string) *PasteCommand {
    return &PasteCommand{
        editor: editor,
        text:   text,
    }
}

func (c *PasteCommand) Execute() error {
    c.position = c.editor.GetCursor()
    c.originalPos = c.position
    
    c.editor.InsertAt(c.position, c.text)
    c.editor.SetCursor(c.position + len(c.text))
    
    log.Printf("Executed: Paste '%s' at position %d", c.text, c.position)
    return nil
}

func (c *PasteCommand) Undo() error {
    c.editor.DeleteAt(c.position, len(c.text))
    c.editor.SetCursor(c.originalPos)
    
    log.Printf("Undone: Paste '%s' at position %d", c.text, c.position)
    return nil
}

func (c *PasteCommand) GetDescription() string {
    return fmt.Sprintf("Paste '%s'", c.text)
}

// Invoker - manages command execution and history
type EditorInvoker struct {
    editor      *TextEditor
    history     []Command
    currentPos  int
    clipboard   string
}

func NewEditorInvoker(editor *TextEditor) *EditorInvoker {
    return &EditorInvoker{
        editor:     editor,
        history:    make([]Command, 0),
        currentPos: -1,
    }
}

func (i *EditorInvoker) ExecuteCommand(command Command) error {
    err := command.Execute()
    if err != nil {
        return err
    }
    
    // Remove any commands after current position (for redo functionality)
    i.history = i.history[:i.currentPos+1]
    
    // Add new command to history
    i.history = append(i.history, command)
    i.currentPos++
    
    return nil
}

func (i *EditorInvoker) Undo() error {
    if i.currentPos < 0 {
        return fmt.Errorf("nothing to undo")
    }
    
    command := i.history[i.currentPos]
    err := command.Undo()
    if err != nil {
        return err
    }
    
    i.currentPos--
    return nil
}

func (i *EditorInvoker) Redo() error {
    if i.currentPos >= len(i.history)-1 {
        return fmt.Errorf("nothing to redo")
    }
    
    i.currentPos++
    command := i.history[i.currentPos]
    return command.Execute()
}

func (i *EditorInvoker) GetHistory() []string {
    var descriptions []string
    for idx, command := range i.history {
        marker := "   "
        if idx == i.currentPos {
            marker = "-> "
        }
        descriptions = append(descriptions, marker+command.GetDescription())
    }
    return descriptions
}

// Macro command - composite command
type MacroCommand struct {
    commands    []Command
    description string
}

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

func (m *MacroCommand) Execute() error {
    for _, command := range m.commands {
        if err := command.Execute(); err != nil {
            // Rollback executed commands
            for i := len(m.commands) - 1; i >= 0; i-- {
                m.commands[i].Undo()
            }
            return err
        }
    }
    return nil
}

func (m *MacroCommand) Undo() error {
    // Undo in reverse order
    for i := len(m.commands) - 1; i >= 0; i-- {
        if err := m.commands[i].Undo(); err != nil {
            return err
        }
    }
    return nil
}

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

func main() {
    // Create editor and invoker
    editor := NewTextEditor()
    invoker := NewEditorInvoker(editor)
    
    fmt.Println("=== Text Editor with Command Pattern ===")
    
    // Execute some commands
    typeHello := NewTypeCommand(editor, "Hello")
    invoker.ExecuteCommand(typeHello)
    fmt.Printf("Content: '%s', Cursor: %d\n", editor.GetContent(), editor.GetCursor())
    
    typeSpace := NewTypeCommand(editor, " ")
    invoker.ExecuteCommand(typeSpace)
    
    typeWorld := NewTypeCommand(editor, "World")
    invoker.ExecuteCommand(typeWorld)
    fmt.Printf("Content: '%s', Cursor: %d\n", editor.GetContent(), editor.GetCursor())
    
    // Copy some text
    copyCmd := NewCopyCommand(editor, 0, 5, &invoker.clipboard) // Copy "Hello"
    invoker.ExecuteCommand(copyCmd)
    
    // Add more text
    typeExclamation := NewTypeCommand(editor, "!")
    invoker.ExecuteCommand(typeExclamation)
    fmt.Printf("Content: '%s', Cursor: %d\n", editor.GetContent(), editor.GetCursor())
    
    // Paste the copied text
    pasteCmd := NewPasteCommand(editor, invoker.clipboard)
    invoker.ExecuteCommand(pasteCmd)
    fmt.Printf("Content: '%s', Cursor: %d\n", editor.GetContent(), editor.GetCursor())
    
    // Delete some characters
    deleteCmd := NewDeleteCommand(editor, 3)
    invoker.ExecuteCommand(deleteCmd)
    fmt.Printf("Content: '%s', Cursor: %d\n", editor.GetContent(), editor.GetCursor())
    
    // Show command history
    fmt.Println("\n=== Command History ===")
    for _, desc := range invoker.GetHistory() {
        fmt.Println(desc)
    }
    
    // Test undo functionality
    fmt.Println("\n=== Testing Undo ===")
    invoker.Undo()
    fmt.Printf("After undo: '%s', Cursor: %d\n", editor.GetContent(), editor.GetCursor())
    
    invoker.Undo()
    fmt.Printf("After undo: '%s', Cursor: %d\n", editor.GetContent(), editor.GetCursor())
    
    // Test redo functionality
    fmt.Println("\n=== Testing Redo ===")
    invoker.Redo()
    fmt.Printf("After redo: '%s', Cursor: %d\n", editor.GetContent(), editor.GetCursor())
    
    // Test macro command
    fmt.Println("\n=== Testing Macro Command ===")
    editor.SetCursor(len(editor.GetContent())) // Move to end
    
    macroCommands := []Command{
        NewTypeCommand(editor, " - "),
        NewTypeCommand(editor, "Macro"),
        NewTypeCommand(editor, " Test"),
    }
    
    macro := NewMacroCommand("Add suffix", macroCommands...)
    invoker.ExecuteCommand(macro)
    fmt.Printf("After macro: '%s', Cursor: %d\n", editor.GetContent(), editor.GetCursor())
    
    // Undo the entire macro
    invoker.Undo()
    fmt.Printf("After macro undo: '%s', Cursor: %d\n", editor.GetContent(), editor.GetCursor())
    
    // Final history
    fmt.Println("\n=== Final Command History ===")
    for _, desc := range invoker.GetHistory() {
        fmt.Println(desc)
    }
}

Advanced Command Pattern with Queuing

Here’s how you can implement command queuing and batch processing:

type CommandQueue struct {
    commands []Command
    mutex    sync.Mutex
}

func NewCommandQueue() *CommandQueue {
    return &CommandQueue{
        commands: make([]Command, 0),
    }
}

func (q *CommandQueue) Enqueue(command Command) {
    q.mutex.Lock()
    defer q.mutex.Unlock()
    q.commands = append(q.commands, command)
}

func (q *CommandQueue) ExecuteAll() error {
    q.mutex.Lock()
    defer q.mutex.Unlock()
    
    for _, command := range q.commands {
        if err := command.Execute(); err != nil {
            return err
        }
    }
    
    q.commands = q.commands[:0] // Clear queue
    return nil
}

func (q *CommandQueue) Size() int {
    q.mutex.Lock()
    defer q.mutex.Unlock()
    return len(q.commands)
}

// Async command processor
type AsyncCommandProcessor struct {
    queue   *CommandQueue
    workers int
    done    chan bool
}

func NewAsyncCommandProcessor(workers int) *AsyncCommandProcessor {
    return &AsyncCommandProcessor{
        queue:   NewCommandQueue(),
        workers: workers,
        done:    make(chan bool),
    }
}

func (p *AsyncCommandProcessor) Start() {
    for i := 0; i < p.workers; i++ {
        go p.worker()
    }
}

func (p *AsyncCommandProcessor) worker() {
    for {
        select {
        case <-p.done:
            return
        default:
            if p.queue.Size() > 0 {
                p.queue.ExecuteAll()
            }
            time.Sleep(10 * time.Millisecond)
        }
    }
}

func (p *AsyncCommandProcessor) Stop() {
    close(p.done)
}

func (p *AsyncCommandProcessor) Submit(command Command) {
    p.queue.Enqueue(command)
}

Real-world Use Cases

Here’s where I commonly use the Command pattern in Go:

  1. GUI Applications: Button clicks, menu actions, keyboard shortcuts
  2. Database Operations: Transactional operations with rollback capability
  3. API Requests: Queuing, retrying, and batching HTTP requests
  4. Job Processing: Background job queues with retry and failure handling
  5. Game Development: Player actions, AI behaviors, replay systems

Benefits of Command Pattern

  1. Decoupling: Separates the object that invokes the operation from the one that performs it
  2. Undo/Redo: Easy to implement undo and redo functionality
  3. Queuing: Commands can be queued, logged, and executed later
  4. Macro Operations: Combine multiple commands into composite operations
  5. Logging and Auditing: Easy to log all operations for debugging or compliance

Caveats

While the Command pattern is powerful, consider these limitations:

  1. Memory Usage: Storing command history can consume significant memory
  2. Complexity: Can add unnecessary complexity for simple operations
  3. Performance: Additional object creation and method calls add overhead
  4. State Management: Commands must carefully manage state for proper undo/redo
  5. Interface Proliferation: Many small command classes can clutter the codebase

Thank you

Thank you for reading! The Command pattern is excellent for building flexible, undoable, and queueable operations in Go. It’s particularly useful when you need to decouple the request from its execution or implement complex undo/redo functionality. Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!