Design Patterns

Command

Encapsula uma requisição como objeto, permitindo undo, filas e log de operações

Conceito e intenção

Command encapsula uma requisição como um objeto independente. Essa transformação permite parametrizar clientes com operações diferentes, enfileirar ou registrar operações, e — o caso de uso mais poderoso — implementar operações reversíveis com undo/redo.

O problema motivador é o acoplamento entre quem dispara a ação e quem a executa. Em editores de texto, o botão “Negrito” e o atalho Ctrl+B precisam executar a mesma operação “tornar texto negrito”. Sem Command, ambos chamam o método diretamente — lógica duplicada e impossibilidade de desfazer. Com Command, o objeto BoldCommand encapsula a operação, o estado necessário para revertê-la, e pode ser enfileirado, serializado, ou executado mais tarde.

A distinção central: Command separa o que fazer (Command) de quando e como fazer (Invoker) e quem faz (Receiver).


Estrutura

┌──────────┐    ┌───────────────────────┐
│  Client  │───▶│       Invoker         │
└──────────┘    │  - history: Deque     │
                │  + execute(Command)   │
                │  + undo()             │
                └───────────┬───────────┘
                            │ chama
                ┌───────────▼────────────┐
                │    <<interface>>       │
                │       Command          │
                │  + execute()           │
                │  + undo()              │
                └────────────────────────┘

               ┌────────────┴──────────────┐
        ┌──────┴──────┐            ┌────────┴──────┐
        │ CmdConcrA   │            │  CmdConcrB    │
        │  - receiver │            │  - receiver   │
        │  - state    │            │  - state      │
        └─────────────┘            └───────────────┘

Participantes:

  • Command — interface com execute() e undo()
  • ConcreteCommand — liga o Receiver ao conjunto de ações; armazena estado para undo
  • Invoker — aciona o Command; mantém histórico para undo/redo
  • Receiver — objeto que sabe como executar a operação real (a lógica de negócio)
  • Client — cria o Command e configura o Receiver

Implementação Java — transferências bancárias com undo/redo

// Interface do Command com suporte a undo
public interface FinancialCommand {
    void execute();
    void undo();
    String getDescription();
}

// Receiver — a lógica de negócio real
public class Account {
    private final String id;
    private BigDecimal balance;

    public Account(String id, BigDecimal initialBalance) {
        this.id      = id;
        this.balance = initialBalance;
    }

    public void debit(BigDecimal amount) {
        if (balance.compareTo(amount) < 0) {
            throw new InsufficientFundsException("Saldo insuficiente em " + id);
        }
        this.balance = balance.subtract(amount);
    }

    public void credit(BigDecimal amount) {
        this.balance = balance.add(amount);
    }

    public BigDecimal getBalance() { return balance; }
    public String getId()          { return id; }
}

// Command concreto: transferência entre contas
public class TransferCommand implements FinancialCommand {
    private final Account source;
    private final Account destination;
    private final BigDecimal amount;
    private final String description;
    private final Instant executedAt;

    public TransferCommand(Account source, Account destination, BigDecimal amount) {
        this.source      = source;
        this.destination = destination;
        this.amount      = amount;
        this.description = String.format("Transferência R$%.2f de %s para %s",
            amount, source.getId(), destination.getId());
        this.executedAt  = Instant.now();
    }

    @Override
    public void execute() {
        source.debit(amount);
        destination.credit(amount);
    }

    @Override
    public void undo() {
        // Operação inversa exata
        destination.debit(amount);
        source.credit(amount);
    }

    @Override
    public String getDescription() { return description; }
}

// Command concreto: depósito
public class DepositCommand implements FinancialCommand {
    private final Account account;
    private final BigDecimal amount;
    private boolean executed = false;

    public DepositCommand(Account account, BigDecimal amount) {
        this.account = account;
        this.amount  = amount;
    }

    @Override
    public void execute() {
        account.credit(amount);
        this.executed = true;
    }

    @Override
    public void undo() {
        if (!executed) throw new IllegalStateException("Comando não foi executado");
        account.debit(amount);
    }

    @Override
    public String getDescription() {
        return String.format("Depósito R$%.2f em %s", amount, account.getId());
    }
}

// Macro Command — compõe múltiplos comandos em um único
public class MacroCommand implements FinancialCommand {
    private final List<FinancialCommand> commands;
    private final String description;

    public MacroCommand(String description, List<FinancialCommand> commands) {
        this.commands    = List.copyOf(commands);
        this.description = description;
    }

    @Override
    public void execute() {
        // Executa em sequência — desfaz os já executados se algum falhar
        List<FinancialCommand> executed = new ArrayList<>();
        try {
            for (FinancialCommand cmd : commands) {
                cmd.execute();
                executed.add(cmd);
            }
        } catch (Exception e) {
            // Rollback dos comandos já executados em ordem inversa
            Collections.reverse(executed);
            executed.forEach(cmd -> {
                try { cmd.undo(); } catch (Exception ignored) {}
            });
            throw e;
        }
    }

    @Override
    public void undo() {
        // Desfaz em ordem inversa
        List<FinancialCommand> reversed = new ArrayList<>(commands);
        Collections.reverse(reversed);
        reversed.forEach(FinancialCommand::undo);
    }

    @Override
    public String getDescription() { return description; }
}

// Invoker — executa e mantém histórico completo
public class TransactionHistory {
    private final Deque<FinancialCommand> executed  = new ArrayDeque<>();
    private final Deque<FinancialCommand> undone    = new ArrayDeque<>();
    private final AuditLog auditLog;

    public TransactionHistory(AuditLog auditLog) {
        this.auditLog = auditLog;
    }

    public void execute(FinancialCommand command) {
        command.execute();
        executed.push(command);
        undone.clear(); // redo stack é invalidado após nova operação
        auditLog.record(command.getDescription(), "EXECUTED");
    }

    public boolean undo() {
        if (executed.isEmpty()) return false;
        FinancialCommand command = executed.pop();
        command.undo();
        undone.push(command);
        auditLog.record(command.getDescription(), "UNDONE");
        return true;
    }

    public boolean redo() {
        if (undone.isEmpty()) return false;
        FinancialCommand command = undone.pop();
        command.execute();
        executed.push(command);
        auditLog.record(command.getDescription(), "REDONE");
        return true;
    }

    public List<String> getHistory() {
        return executed.stream()
            .map(FinancialCommand::getDescription)
            .collect(Collectors.toList());
    }
}

// Uso
Account savings  = new Account("poupanca", new BigDecimal("5000.00"));
Account checking = new Account("corrente", new BigDecimal("1000.00"));

TransactionHistory history = new TransactionHistory(auditLog);

// Executa operações
history.execute(new DepositCommand(checking, new BigDecimal("500.00")));
history.execute(new TransferCommand(checking, savings, new BigDecimal("300.00")));

// Desfaz a última transferência
history.undo();

// Refaz a transferência
history.redo();

// Macro: operações compostas que desfazem juntas
history.execute(new MacroCommand("Folha de pagamento", List.of(
    new TransferCommand(checking, empAccount1, new BigDecimal("2500.00")),
    new TransferCommand(checking, empAccount2, new BigDecimal("3200.00")),
    new TransferCommand(checking, empAccount3, new BigDecimal("1800.00"))
)));

Command Queue — operações assíncronas

// Command como unidade de trabalho enfileirável
public interface AsyncTask {
    void execute();
    int getPriority();   // 1 = alta, 5 = baixa
    String getTaskId();
}

// Processador de fila com prioridade
@Component
public class TaskProcessor {
    private final BlockingQueue<AsyncTask> queue = new PriorityBlockingQueue<>(
        100, Comparator.comparingInt(AsyncTask::getPriority)
    );

    public void submit(AsyncTask task) {
        queue.offer(task);
    }

    @Scheduled(fixedDelay = 100)
    public void processNext() throws InterruptedException {
        AsyncTask task = queue.poll(50, TimeUnit.MILLISECONDS);
        if (task != null) {
            try {
                task.execute();
            } catch (Exception e) {
                log.error("Falha na task {}: {}", task.getTaskId(), e.getMessage());
                // Estratégia de retry: resubmit com prioridade reduzida
            }
        }
    }
}

// Tarefas concretas como Commands
public class SendReportTask implements AsyncTask {
    private final String reportId;
    private final String recipientEmail;
    private final ReportService reportService;

    @Override
    public void execute() {
        Report report = reportService.generate(reportId);
        reportService.sendByEmail(report, recipientEmail);
    }

    @Override
    public int getPriority() { return 3; }

    @Override
    public String getTaskId() { return "report-" + reportId; }
}

Implementação TypeScript

// Interface do Command
interface Command<T = void> {
  execute(): T;
  undo(): void;
  readonly description: string;
}

// Receiver
class BankAccount {
  constructor(
    readonly id: string,
    private balance: number
  ) {}

  debit(amount: number): void {
    if (this.balance < amount) throw new Error(`Saldo insuficiente em ${this.id}`);
    this.balance -= amount;
  }

  credit(amount: number): void {
    this.balance += amount;
  }

  getBalance(): number { return this.balance; }
}

// Concrete Command
class TransferCommand implements Command {
  readonly description: string;

  constructor(
    private readonly from: BankAccount,
    private readonly to: BankAccount,
    private readonly amount: number
  ) {
    this.description = `Transferência R$${amount.toFixed(2)} de ${from.id} para ${to.id}`;
  }

  execute(): void {
    this.from.debit(this.amount);
    this.to.credit(this.amount);
  }

  undo(): void {
    this.to.debit(this.amount);
    this.from.credit(this.amount);
  }
}

// Invoker com undo/redo
class CommandHistory {
  private readonly executed: Command[] = [];
  private readonly undone: Command[]   = [];

  execute(command: Command): void {
    command.execute();
    this.executed.push(command);
    this.undone.length = 0; // invalida redo stack
  }

  undo(): boolean {
    const command = this.executed.pop();
    if (!command) return false;
    command.undo();
    this.undone.push(command);
    return true;
  }

  redo(): boolean {
    const command = this.undone.pop();
    if (!command) return false;
    command.execute();
    this.executed.push(command);
    return true;
  }

  getHistory(): string[] {
    return this.executed.map(c => c.description);
  }
}

No mundo real

java.lang.Runnable e Callable são a forma mais simples de Command no Java SDK. ExecutorService.submit(callable) enfileira Commands para execução assíncrona.

Spring Batch Tasklet — cada Tasklet é um Command que o Spring executa, monitora e pode retomar após falha.

javax.swing.Action — interface de Command no Swing que combina ação + metadata (ícone, tooltip, atalho). Reutilizável em botão, menu e atalho de teclado simultaneamente.

Git implementa um modelo baseado em Command — cada commit, merge e rebase é uma operação que pode ser revertida (git revert, git reset).

Redux / Vuex actions — actions são Commands imutáveis que descrevem intenção de mudança. O state pode ser reconstruído “repetindo” os commands (time-travel debugging).

CQRS (Command Query Responsibility Segregation) — toda mutação do sistema é modelada como Command explícito, separando leitura de escrita. Event Sourcing armazena todos os Commands para reconstruir estado.

UndoManager do Java Swing — Invoker genérico que gerencia stack de UndoableEdit (Commands) com undo/redo.


Quando usar

  • Implementar undo/redo em editores, formulários, ou transações
  • Operações que precisam ser enfileiradas, agendadas ou executadas em thread diferente
  • Audit log — cada operação é um Command serializado e persistido
  • Parametrizar objetos com operações (ex: botões com diferentes ações)
  • Implementar transações compostas que precisam ser revertidas atomicamente

Quando NÃO usar

  • Operações simples sem necessidade de undo, fila ou auditoria — adiciona cerimônia sem benefício
  • Quando o Command acumula lógica de domínio — a lógica pertence ao Receiver (domínio), não ao Command (coordenação)
  • Para simples callbacks — lambdas e Runnable são suficientes quando não precisa de undo ou estado
  • Quando o número de Command classes explode sem padrão — considere macro commands ou repositório de operações

Combinações com outros padrões

Command + Memento: Memento captura o estado do Receiver antes do Command executar. O undo restaura o Memento em vez de executar operação inversa — útil quando a reversão é complexa:

public void execute() {
    this.savedState = receiver.saveState(); // Memento
    receiver.doOperation();
}
public void undo() {
    receiver.restoreState(savedState);
}

Command + Chain of Responsibility: Commands podem ser passados por uma cadeia de handlers que decidem como processá-los — útil para roteamento de comandos a diferentes serviços.

Command + Observer: Após executar, o Command pode publicar um evento (Observer) notificando interessados. O Invoker emite CommandExecuted event; múltiplos observers reagem.

Command + Composite (Macro Command): Como mostrado no exemplo, comandos compostos permitem tratar grupos de operações como um único Command, com undo atômico.