Introdução ao padrão command para operações reversíveis

1. Fundamentos do padrão Command

O padrão Command é um dos 23 padrões de design do catálogo GoF (Gang of Four), classificado como padrão comportamental. Sua definição original estabelece que ele "encapsula uma solicitação como um objeto, permitindo parametrizar clientes com diferentes solicitações, enfileirar ou registrar solicitações e suportar operações reversíveis".

O propósito principal do padrão é transformar requisições em objetos independentes, que contêm todas as informações necessárias para executar uma ação futuramente. Essa abordagem permite separar quem invoca a operação de quem sabe como executá-la.

A diferença fundamental entre um Command simples e um Command com suporte a reversão está na adição do método undo(). Enquanto o Command simples apenas encapsula a execução (execute()), o Command reversível também armazena o estado anterior para restaurá-lo quando necessário.

2. Estrutura clássica do padrão Command

A estrutura clássica do padrão Command é composta por quatro elementos principais:

  • Command (Interface): Declara os métodos execute() e undo()
  • ConcreteCommand: Implementa a interface, armazenando referências ao Receiver e os parâmetros necessários
  • Receiver: Contém a lógica de negócio real que será executada
  • Invoker: Solicita a execução do comando, geralmente mantendo um histórico

O fluxo de comunicação segue esta sequência: o Cliente cria um ConcreteCommand, associa-o a um Receiver, e passa o comando para o Invoker. Quando o Invoker chama execute(), o ConcreteCommand delega a ação ao Receiver.

// Interface Command
public interface Command {
    void execute();
    void undo();
}

// Receiver - contém a lógica real
public class TextEditor {
    private StringBuilder content = new StringBuilder();

    public void insert(String text, int position) {
        content.insert(position, text);
    }

    public void delete(int start, int end) {
        content.delete(start, end);
    }

    public String getContent() {
        return content.toString();
    }
}

3. Implementando operações reversíveis com Command

Para estender o padrão Command com suporte a reversão, cada ConcreteCommand deve armazenar o estado anterior da operação antes de executá-la. No método undo(), esse estado é restaurado.

Vamos implementar um exemplo prático de edição de texto com operações de inserir e remover:

// ConcreteCommand para inserção
public class InsertCommand implements Command {
    private TextEditor editor;
    private String text;
    private int position;

    public InsertCommand(TextEditor editor, String text, int position) {
        this.editor = editor;
        this.text = text;
        this.position = position;
    }

    @Override
    public void execute() {
        editor.insert(text, position);
    }

    @Override
    public void undo() {
        // Remove o texto inserido
        editor.delete(position, position + text.length());
    }
}

// ConcreteCommand para remoção
public class DeleteCommand implements Command {
    private TextEditor editor;
    private int start;
    private int end;
    private String deletedText; // Estado anterior armazenado

    public DeleteCommand(TextEditor editor, int start, int end) {
        this.editor = editor;
        this.start = start;
        this.end = end;
    }

    @Override
    public void execute() {
        // Armazena o texto antes de deletar
        deletedText = editor.getContent().substring(start, end);
        editor.delete(start, end);
    }

    @Override
    public void undo() {
        // Insere o texto deletado de volta
        editor.insert(deletedText, start);
    }
}

4. Gerenciamento de histórico de comandos

O gerenciamento de histórico é implementado usando uma pilha (stack) para rastrear comandos executados. Para suporte a redo, utilizamos uma pilha auxiliar.

public class CommandHistory {
    private Stack<Command> undoStack = new Stack<>();
    private Stack<Command> redoStack = new Stack<>();
    private static final int MAX_HISTORY = 50; // Limitação de memória

    public void executeCommand(Command command) {
        command.execute();
        undoStack.push(command);
        redoStack.clear(); // Redo é limpo após novo comando

        // Estratégia de limpeza: remove comandos antigos
        if (undoStack.size() > MAX_HISTORY) {
            undoStack.remove(0);
        }
    }

    public void undo() {
        if (!undoStack.isEmpty()) {
            Command command = undoStack.pop();
            command.undo();
            redoStack.push(command);
        }
    }

    public void redo() {
        if (!redoStack.isEmpty()) {
            Command command = redoStack.pop();
            command.execute();
            undoStack.push(command);
        }
    }
}

5. Casos de uso práticos em sistemas reais

O padrão Command com reversão é amplamente utilizado em diversos domínios:

Editores de texto e IDEs: A funcionalidade Ctrl+Z (undo) e Ctrl+Y (redo) é implementada com este padrão. Cada ação do usuário (digitar, deletar, formatar) é encapsulada como um comando.

Sistemas de transações bancárias: Operações de débito e crédito são comandos que podem ser revertidos em caso de falha, garantindo consistência dos dados.

Jogos digitais: Movimentos de jogadores em jogos de tabuleiro ou estratégia são comandos que permitem desfazer ações. Sistemas de replay também utilizam este padrão.

6. Vantagens e desvantagens do padrão

Vantagens:
- Desacoplamento entre invocador e executor
- Facilidade de extensão (novos comandos podem ser adicionados sem modificar código existente)
- Suporte nativo a undo/redo
- Possibilidade de enfileiramento e execução assíncrona

Desvantagens:
- Aumento da complexidade do código
- Consumo de memória para armazenar histórico de comandos
- Dificuldade em gerenciar estado de objetos mutáveis

Quando NÃO usar Command:
- Quando as operações são simples e não precisam de reversão
- Quando o overhead de criar objetos para cada ação é proibitivo
- Em sistemas com requisitos de memória extremamente restritos

7. Comparação com padrões relacionados

Command vs Strategy: Ambos encapsulam comportamento, mas com intenções diferentes. Command encapsula uma ação (o que fazer), enquanto Strategy encapsula um algoritmo (como fazer). Command suporta undo/redo; Strategy não.

Command vs Memento: Memento foca em capturar e armazenar o estado interno de um objeto para restauração posterior. Command encapsula uma requisição inteira. Eles podem ser combinados: Command usa Memento para armazenar estado anterior.

Command vs Chain of Responsibility: Chain of Responsibility passa uma requisição por uma cadeia de handlers até que um a processe. Command encapsula a requisição e a entrega diretamente a um handler específico.

8. Boas práticas e armadilhas comuns

Cuidados com referências a objetos mutáveis: Ao armazenar estado para undo, faça cópias defensivas dos objetos. Se o estado compartilhar referências com outros objetos, alterações externas podem corromper o histórico.

// Boa prática: cópia defensiva
public class SafeDeleteCommand implements Command {
    private String snapshot;

    @Override
    public void execute() {
        snapshot = new String(editor.getContent()); // Cópia defensiva
        // ... executa a deleção
    }
}

Testabilidade: Comandos devem ser testados isoladamente. Cada comando é um objeto independente que pode ser instanciado e testado sem depender do Invoker.

Integração com Composite Command: Para operações complexas, use Composite Command (Macro Command) que agrupa vários comandos em um único comando composto, permitindo undo/redo atômico de operações compostas.

public class MacroCommand implements Command {
    private List<Command> commands = new ArrayList<>();

    public void addCommand(Command command) {
        commands.add(command);
    }

    @Override
    public void execute() {
        for (Command command : commands) {
            command.execute();
        }
    }

    @Override
    public void undo() {
        // Desfaz na ordem reversa
        ListIterator<Command> iterator = commands.listIterator(commands.size());
        while (iterator.hasPrevious()) {
            iterator.previous().undo();
        }
    }
}

O padrão Command para operações reversíveis é uma ferramenta poderosa no arsenal de qualquer desenvolvedor. Quando aplicado corretamente, oferece flexibilidade, extensibilidade e uma base sólida para implementar funcionalidades críticas como undo/redo em sistemas complexos.

Referências