Design de software

Implementando undo com o padrão Command do Swift

Ao desenvolver um app, o pedido para “adicionar só um botão de undo” aparece com mais frequência do que você imagina.

4 min de leitura
Imagem de capa de Implementando undo com o padrão Command do Swift

Ao desenvolver um app, o pedido para “adicionar só um botão de undo” aparece com mais frequência do que você imagina.

Quando você tenta adicionar undo, o código pode virar uma verdadeira bagunça. Por causa de um único botão, a lógica de reversão acaba espalhada pelo código de exclusão, movimentação e edição.

Quem organiza isso é justamente o padrão Command do Swift.

Neste artigo, vamos entender por que o padrão Command é a base do undo e revisar a estrutura mínima de código para usá-lo na prática. O foco está em um formato que você pode adicionar imediatamente ao projeto, e não em teoria complexa.

O que é o padrão Command? Resumo em uma linha

Vamos começar com uma definição rápida.

O padrão Command encapsula em um objeto a ação de “fazer algo” e reúne execução e cancelamento em uma única unidade.

Em vez de executar a lógica diretamente ao tocar em um botão, criamos um objeto Command que diz: “execute esta ação”.

Esse comando contém tanto a forma de executar (execute) quanto a forma de desfazer (undo).

Em resumo:

  1. Transforme ações em objetos — exclusão, movimentação e entrada viram comandos individuais
  2. Cada comando mantém execute() e undo() em par
  3. Empilhe os comandos executados
  4. Para desfazer, retire um comando da pilha e chame undo()

Graças a essa estrutura, a lógica de reversão fica dentro de cada comando, em vez de espalhada pelo código.


Vamos criar primeiro o protocolo Command em Swift

Primeiro, definimos o contrato que todo comando deve seguir: o protocolo.

Este é o esqueleto mínimo que exige execução e undo.

protocol Command {
    func execute()   // Executar ação
    func undo()      // Desfazer execução
}

Só esses dois métodos são necessários.

Agora vamos criar um comando concreto com uma ação real. Por exemplo, um comando que adiciona texto.

O código abaixo adiciona texto e o remove novamente quando é desfeito.

final class AddTextCommand: Command {
    private let document: Document
    private let text: String

    init(document: Document, text: String) {
        self.document = document
        self.text = text
    }

    func execute() { document.content += text }
    func undo()    { document.content.removeLast(text.count) }
}

execute adiciona o texto, e undo remove exatamente o que foi adicionado. Ter execução e undo no mesmo objeto é todo o padrão.

O protocolo Command começa com apenas duas linhas
O protocolo Command começa com apenas duas linhas

A chave do undo é a pilha

Depois de criar os comandos, você precisa de um “gerenciador” para cuidar deles. Esse papel costuma ser chamado de Invoker.

O gerenciador empilha os comandos executados, um após o outro.

Ao tocar em undo, retire o comando mais recente e chame undo(). Desfazer primeiro o que foi feito por último é a ordem natural do undo.

Este é o esqueleto do gerenciador.

final class CommandManager {
    private var undoStack: [Command] = []

    func run(_ command: Command) {
        command.execute()
        undoStack.append(command)   // Salvar na pilha após a execução
    }

    func undo() {
        guard let last = undoStack.popLast() else { return }
        last.undo()                 // Desfazer começando pelo último
    }
}

Agora o uso fica muito simples.

Em buttonTapped, chame manager.run(command); no botão de undo, basta chamar manager.undo().

O CommandManager cuida da ordem de reversão e do gerenciamento da pilha, mantendo o código da tela limpo.

As três peças do Command se conectam assim
As três peças do Command se conectam assim

Se você também precisar de redo, armazene os comandos desfeitos separadamente em redoStack. Como a estrutura é simétrica, é fácil estendê-la.


Quando usar e quando evitar o padrão Command?

O padrão Command também não é uma solução universal. Veja na tabela quando ele se encaixa e quando é exagero.

Situação Padrão Command
É necessário undo e redo Muito adequado
É preciso manter o histórico de operações Adequado
Enfileirar ações para executar depois Adequado
Um botão simples sem nada para desfazer Exagero; use uma função

O padrão Command se destaca quando você precisa de undo ou histórico. Apps de desenho, editores de texto, cancelamento de pagamentos e undo em jogos são exemplos típicos.

Por outro lado, aplicar o padrão Command a um botão que apenas navega entre telas só aumenta o código.

Transformar todos os botões em comandos pode gerar rapidamente muitos arquivos, então aplique o padrão apenas a ações que precisam ser desfeitas.

Uma estrutura que retira e desfaz os cartões um a um, começando pelo topo
Uma estrutura que retira e desfaz os cartões um a um, começando pelo topo

Resumo

Adicionar undo pode parecer complicado, mas, com o padrão Command como base, tudo funciona de forma mais organizada do que você espera.

Comece aplicando as três peças de hoje — protocolo, comandos concretos e gerenciador — a uma tela pequena. Quando a estrutura ficar familiar, as extensões para redo e histórico virão naturalmente. Adicione um botão de undo ao seu app com confiança.

Continue lendo