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()eundo() - 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
Runnablesã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.