Padrão Comportamental (GoF)

Memento

Captura e externaliza o estado interno de um objeto sem violar seu encapsulamento, permitindo restaurá-lo a esse estado posteriormente — a base do mecanismo de undo por snapshot completo.

Intenção

Capturar o estado interno de um objeto num objeto separado (Memento), sem expor os detalhes de implementação desse estado ao mundo externo. O Memento pode ser armazenado e posteriormente usado para restaurar o objeto ao estado capturado — implementando undo, checkpoints, transações e histórico de estados.

Catalogado pelo GoF (1994) como padrão comportamental, o Memento preserva o encapsulamento do Originator: quem armazena o Memento (o Caretaker) não sabe o que há dentro dele — apenas guarda e devolve. Apenas o Originator conhece o formato do estado e sabe como criar e restaurar um Memento. Isso distingue o padrão de uma simples serialização pública do estado: a interface do Memento é opaca para qualquer objeto que não seja o Originator que o criou.

Problema

Continuando o domínio do editor de texto do padrão Command: queremos implementar undo. A abordagem ingênua é expor o estado do editor publicamente para que um objeto externo possa salvá-lo:

// Abordagem ingênua — viola encapsulamento:
class Editor {
  conteudo: string = '';  // público para o Caretaker salvar
  cursorPos: number = 0;  // público para o Caretaker salvar
  selecao: [number, number] | null = null; // público
}

// O Caretaker acessa e conhece os internos do Editor — forte acoplamento.
// Se o Editor mudar seus campos, o Caretaker quebra.
const estadoSalvo = {
  conteudo: editor.conteudo,
  cursorPos: editor.cursorPos,
  selecao: editor.selecao,
};

O problema é duplo: o Caretaker (quem armazena o estado) precisa conhecer os detalhes internos do Editor (quem tem o estado), criando acoplamento forte. Qualquer refatoração interna do Editor — renomear um campo, mudar a estrutura da seleção — quebra o Caretaker. O Memento elimina isso: o Editor produz um objeto opaco com seu estado, e o Caretaker apenas o armazena — sem inspecioná-lo.

Distinção crítica: Memento vs Command com undo

Ambos os padrões implementam undo, mas com filosofias diferentes:

  • Command com undo: cada Command encapsula uma ação específica e sabe como revertê-la incrementalmente. Um InserirTextoCommand sabe deletar exatamente o trecho que inseriu. O undo é preciso mas exige que cada Command capture o estado suficiente para sua própria reversão.
  • Memento: captura o estado completo do Originator num snapshot. O undo restaura o estado inteiro — não desfaz uma ação específica, mas retorna o objeto a um ponto anterior no tempo. Mais simples de implementar para objetos com estado rico e interdependente, mas mais custoso em memória (um snapshot por ponto de restauração).

Os dois padrões são complementares: Command pode usar Memento como mecanismo de captura de estado em seu execute(), delegando a responsabilidade de "como salvar o estado" ao próprio Originator.

Solução

O Memento organiza o código em três participantes:

  1. Originator: o objeto cujo estado queremos capturar. Cria Mementos via salvarEstado() e restaura seu estado a partir de um Memento via restaurarEstado(memento). É o único que conhece o conteúdo interno do Memento. Ex.: Editor com conteúdo, posição do cursor e seleção.
  2. Memento: objeto que armazena o snapshot do estado do Originator. Sua interface para o Caretaker é opaca — não expõe getters do estado interno. Apenas o Originator acessa o conteúdo. Em TypeScript, isso pode ser modelado com uma classe interna ou com campos privados e um método de acesso restrito via tipo nominal.
  3. Caretaker: gerencia o histórico de Mementos. Não inspeciona nem modifica o conteúdo dos Mementos — apenas os armazena em ordem e os devolve ao Originator quando o undo é solicitado. Ex.: Historico com pilha de Mementos e métodos salvar() e desfazer().

Estrutura

       Editor (Originator)
  ┌──────────────────────────────────────────────┐
  │ - conteudo: string                           │
  │ - cursorPos: number                          │
  │ - selecao: [number,number] | null            │
  │                                              │
  │ + salvarEstado(): EditorSnapshot             │
  │   → cria Memento com cópia do estado atual   │
  │ + restaurarEstado(s: EditorSnapshot): void   │
  │   → recupera estado do Memento              │
  │ + (operações de edição)                     │
  └──────────────────────────────────────────────┘
       cria e restaura ▲            ▼ armazenado por
                        │            │
       EditorSnapshot (Memento) — interface opaca para o Caretaker
  ┌──────────────────────────────────────────────┐
  │ (campos privados — só o Editor acessa)       │
  │ - conteudo: string                           │
  │ - cursorPos: number                          │
  │ - selecao: [number,number] | null            │
  │                                              │
  │ (sem getters públicos — opaco ao Caretaker)  │
  └──────────────────────────────────────────────┘
                            ▲ guarda e devolve
                            │
         Historico (Caretaker)
  ┌──────────────────────────────────────────────┐
  │ - pilha: EditorSnapshot[]                    │
  │                                              │
  │ + salvar(originator: Editor): void           │
  │   → pilha.push(editor.salvarEstado())        │
  │ + desfazer(originator: Editor): void         │
  │   → editor.restaurarEstado(pilha.pop())      │
  │                                              │
  │ !! Nunca inspeciona o conteúdo do Memento !! │
  └──────────────────────────────────────────────┘


Comparação de abordagens para undo:

  Command + undo incremental          Memento + snapshot
  ─────────────────────────────       ──────────────────────────────
  Cada Command conhece seu undo       Originator conhece seu estado
  Memória: proporcional à ação        Memória: proporcional ao estado
  Preciso para ações atômicas         Simples para estado complexo
  Mais boilerplate por tipo de ação   Menos código, mais memória

Exemplos de código

Exemplo 1 — Editor de texto com undo por snapshot (Memento)

O mesmo domínio do Command, mas agora o undo captura o estado completo do editor num snapshot. Note como o Historico (Caretaker) não acessa nenhum dado interno do EditorSnapshot — apenas o armazena e o devolve. A comparação com Command fica explícita nos comentários.

// ── Memento — snapshot opaco do Editor ────────────────────────
// A interface exposta ao Caretaker é opaca: sem getters públicos.
// Somente o Editor acessa os campos via método de pacote (friend pattern).
class EditorSnapshot {
  // Campos readonly — imutáveis após a criação do snapshot.
  constructor(
    private readonly conteudo: string,
    private readonly cursorPos: number,
    private readonly selecao: readonly [number, number] | null
  ) {}

  // Método restrito: na prática o Editor é o único que o chama.
  // Em linguagens com friend/package-private, isso seria inacessível ao Caretaker.
  _recuperar(): {
    conteudo: string;
    cursorPos: number;
    selecao: readonly [number, number] | null;
  } {
    return {
      conteudo: this.conteudo,
      cursorPos: this.cursorPos,
      selecao: this.selecao ? [...this.selecao] : null,
    };
  }
}

// ── Originator — cria e restaura Mementos ────────────────────
class Editor {
  private conteudo: string = '';
  private cursorPos: number = 0;
  private selecao: [number, number] | null = null;

  // Operações de edição
  inserir(texto: string): void {
    this.conteudo =
      this.conteudo.slice(0, this.cursorPos) +
      texto +
      this.conteudo.slice(this.cursorPos);
    this.cursorPos += texto.length;
  }

  deletarAnterior(): void {
    if (this.cursorPos === 0) return;
    this.conteudo =
      this.conteudo.slice(0, this.cursorPos - 1) +
      this.conteudo.slice(this.cursorPos);
    this.cursorPos--;
  }

  moverCursor(pos: number): void {
    this.cursorPos = Math.max(0, Math.min(pos, this.conteudo.length));
  }

  selecionarIntervalo(inicio: number, fim: number): void {
    this.selecao = [inicio, fim];
  }

  // Cria snapshot do estado COMPLETO
  salvarEstado(): EditorSnapshot {
    return new EditorSnapshot(
      this.conteudo,
      this.cursorPos,
      this.selecao ? [...this.selecao] : null
    );
  }

  // Restaura a partir de um snapshot
  restaurarEstado(snapshot: EditorSnapshot): void {
    const estado = snapshot._recuperar();
    this.conteudo  = estado.conteudo;
    this.cursorPos = estado.cursorPos;
    this.selecao   = estado.selecao ? [...estado.selecao] : null;
  }

  toString(): string {
    const sel = this.selecao
      ? ` [sel ${this.selecao[0]}-${this.selecao[1]}]`
      : '';
    return `"${this.conteudo}" cursor=${this.cursorPos}${sel}`;
  }
}

// ── Caretaker — armazena histórico de snapshots ───────────────
class Historico {
  private readonly pilha: EditorSnapshot[] = [];

  // Salva o estado atual do editor na pilha
  salvar(editor: Editor): void {
    this.pilha.push(editor.salvarEstado());
  }

  // Restaura o último estado salvo
  desfazer(editor: Editor): boolean {
    const snapshot = this.pilha.pop();
    if (!snapshot) return false;
    // O Caretaker devolve o Memento ao Originator — nunca o inspeciona.
    editor.restaurarEstado(snapshot);
    return true;
  }

  tamanho(): number { return this.pilha.length; }
}

// ── Uso ──────────────────────────────────────────────────────
const editor   = new Editor();
const historico = new Historico();

// Estado 1: vazio
historico.salvar(editor);
console.log(`[0] ${editor}`);   // "" cursor=0

editor.inserir('Olá');
historico.salvar(editor);
console.log(`[1] ${editor}`);   // "Olá" cursor=3

editor.inserir(', mundo');
historico.salvar(editor);
console.log(`[2] ${editor}`);   // "Olá, mundo" cursor=10

editor.selecionarIntervalo(4, 9);
historico.salvar(editor);
console.log(`[3] ${editor}`);   // "Olá, mundo" cursor=10 [sel 4-9]

// Undo: volta ao estado 3 (antes da seleção)
historico.desfazer(editor);
console.log(`Undo → ${editor}`); // "Olá, mundo" cursor=10

// Undo: volta ao estado 2 (antes de ", mundo")
historico.desfazer(editor);
console.log(`Undo → ${editor}`); // "Olá" cursor=3

// Undo: volta ao estado 1 (vazio)
historico.desfazer(editor);
console.log(`Undo → ${editor}`); // "" cursor=0

console.log(`Snapshots restantes: ${historico.tamanho()}`); // 0

Exemplo 2 — Configuração de aplicação com checkpoints nomeados

O Memento não se limita a editores. Um Caretaker com suporte a checkpoints nomeados (em vez de uma pilha anônima) permite salvar e restaurar estados com semântica clara — como "configuração de fábrica" vs "configuração do usuário" — sem expor os campos internos do objeto de configuração.

// ── Memento de configuração ───────────────────────────────────
class ConfigSnapshot {
  constructor(
    private readonly dados: Readonly<Record<string, unknown>>
  ) {}

  _recuperar(): Record<string, unknown> {
    return { ...this.dados }; // cópia defensiva
  }
}

// ── Originator ────────────────────────────────────────────────
class ConfiguracaoApp {
  private dados: Record<string, unknown> = {
    tema: 'claro',
    idioma: 'pt-BR',
    fontSize: 14,
    notificacoes: true,
  };

  set(chave: string, valor: unknown): void {
    this.dados[chave] = valor;
  }

  get(chave: string): unknown {
    return this.dados[chave];
  }

  salvarEstado(): ConfigSnapshot {
    return new ConfigSnapshot({ ...this.dados });
  }

  restaurarEstado(snapshot: ConfigSnapshot): void {
    this.dados = snapshot._recuperar();
  }

  exibir(): void {
    console.log(JSON.stringify(this.dados, null, 2));
  }
}

// ── Caretaker com checkpoints nomeados ────────────────────────
class GerenciadorCheckpoints {
  private readonly checkpoints = new Map<string, ConfigSnapshot>();

  salvar(nome: string, config: ConfiguracaoApp): void {
    this.checkpoints.set(nome, config.salvarEstado());
    console.log(`Checkpoint "${nome}" salvo.`);
  }

  restaurar(nome: string, config: ConfiguracaoApp): boolean {
    const snapshot = this.checkpoints.get(nome);
    if (!snapshot) {
      console.warn(`Checkpoint "${nome}" não encontrado.`);
      return false;
    }
    config.restaurarEstado(snapshot);
    console.log(`Checkpoint "${nome}" restaurado.`);
    return true;
  }

  listar(): string[] {
    return [...this.checkpoints.keys()];
  }
}

// ── Uso ──────────────────────────────────────────────────────
const config = new ConfiguracaoApp();
const gerenciador = new GerenciadorCheckpoints();

// Salva o estado de fábrica
gerenciador.salvar('fabrica', config);

// Usuário personaliza
config.set('tema', 'escuro');
config.set('fontSize', 18);
config.set('idioma', 'en-US');
gerenciador.salvar('usuario', config);

// Mais ajustes — sem salvar
config.set('notificacoes', false);
config.set('fontSize', 22);
console.log('\nEstado atual:');
config.exibir();
// { tema: 'escuro', idioma: 'en-US', fontSize: 22, notificacoes: false }

// Restaura configuração do usuário
gerenciador.restaurar('usuario', config);
config.exibir();
// { tema: 'escuro', idioma: 'en-US', fontSize: 18, notificacoes: true }

// Restaura fábrica
gerenciador.restaurar('fabrica', config);
config.exibir();
// { tema: 'claro', idioma: 'pt-BR', fontSize: 14, notificacoes: true }

console.log('Checkpoints disponíveis:', gerenciador.listar());
// ['fabrica', 'usuario']

Quando usar

  • Quando você precisa de undo/redo por snapshot: o caso central do padrão. Se o estado do Originator é suficientemente compacto ou a profundidade de undo é limitada, Memento é mais simples de implementar do que Command com undo incremental para cada tipo de operação.
  • Quando o estado do objeto é rico e interdependente: se múltiplos campos precisam ser restaurados em conjunto para garantir consistência (ex.: conteúdo + cursor + seleção no editor), um snapshot completo é mais seguro do que reverter campo por campo.
  • Quando você precisa de checkpoints ou transações: salvar o estado antes de uma operação potencialmente destrutiva e restaurar em caso de falha — sem expor os internos do objeto a quem faz o gerenciamento.
  • Quando o encapsulamento do Originator deve ser preservado: o Caretaker precisa guardar o estado mas não deve conhecer sua estrutura interna. O Memento cria a barreira de abstração necessária.

Quando evitar

  • Quando o estado do Originator é grande: cada snapshot é uma cópia do estado completo. Para objetos com megabytes de dados, manter um histórico de snapshots pode esgotar a memória. Considere snapshots incrementais, compressão ou Command com undo incremental.
  • Quando a profundidade de undo é ilimitada e o objeto é grande: a combinação de estado grande + muitos níveis de undo torna o Memento proibitivo em memória. Defina um limite máximo de snapshots no Caretaker.
  • Quando linguagens dinâmicas tornam o encapsulamento trivial de violar: em PHP e JavaScript, o encapsulamento do Memento é convencional, não aplicado pelo compilador. Se o Caretaker pode acessar os campos do Memento via reflexão, a barreira conceitual do padrão perde força.

Prós e contras

Prós

  • Preserva o encapsulamento do Originator — o Caretaker não conhece a estrutura interna do estado.
  • Simplifica o Originator: a lógica de undo não fica espalhada em cada operação — é centralizada em salvarEstado/restaurarEstado.
  • Permite undo de múltiplos níveis e checkpoints nomeados com o mesmo mecanismo.
  • O Caretaker pode limitar a profundidade do histórico descartando Mementos antigos — sem alterar o Originator.

Contras

  • Alto consumo de memória para Originators com estado grande — cada snapshot é uma cópia completa.
  • O encapsulamento do Memento é difícil de aplicar em linguagens sem suporte a classes internas ou pacotes (como TypeScript e PHP) — é convencional, não forçado pelo compilador.
  • Se o Originator muda sua estrutura interna, o formato dos Mementos existentes pode se tornar incompatível (problema de versionamento).
  • Requer cópia defensiva do estado para evitar que o Memento compartilhe referências mutáveis com o Originator.

Armadilhas comuns

1. Caretaker que inspeciona o Memento (viola encapsulamento)

O erro fundamental do padrão: o Caretaker chama getters do Memento para ler campos, exibir informações ou tomar decisões. Isso cria acoplamento entre o Caretaker e a estrutura interna do Originator — exatamente o que o padrão visa evitar. O Caretaker deve tratar o Memento como uma caixa preta: recebe, armazena, devolve. Nada mais.

Sinal de alerta: se o Caretaker tem um if (snapshot.getConteudo().length > 0) em algum lugar, o encapsulamento foi violado.

2. Referências mutáveis compartilhadas (snapshot raso)

A armadilha técnica mais crítica: o salvarEstado() copia referências em vez de valores. Se o estado do Originator contém objetos ou arrays mutáveis e o Memento guarda a referência (não uma cópia profunda), o snapshot refletirá mudanças posteriores — tornando-o inútil para restauração. A regra de ouro: o Memento deve ser imutável e conter apenas cópias dos dados, nunca as referências originais.

// ERRADO: referência compartilhada — o snapshot muda com o Originator
class EditorSnapshot {
  constructor(readonly dados: string[]) {} // referência!
}
const snap = new EditorSnapshot(editor.linhas); // editor.linhas e snap.dados apontam para o mesmo array
editor.linhas.push('nova linha'); // corrompeu o snapshot!

// CORRETO: cópia defensiva
class EditorSnapshot {
  constructor(readonly dados: readonly string[]) {}
}
const snap = new EditorSnapshot([...editor.linhas]); // cópia — independente

3. Mementos incompatíveis após refatoração (versionamento)

Se o Originator é serializado para persistência (arquivos de save de jogo, sessões de usuário) e o formato do Memento muda com uma refatoração, snapshots antigos podem se tornar irrestauráveis. Considere incluir um número de versão no Memento e implementar migração quando o formato muda — especialmente se Mementos são persistidos além da sessão atual.

4. Histórico sem limite de tamanho

Um Caretaker que aceita Mementos ilimitadamente pode esgotar memória gradualmente. Defina um limite máximo (ex.: 50 snapshots) no Caretaker e descarte os mais antigos quando o limite for atingido. Isso é uma decisão do Caretaker — o Originator e o Memento não precisam saber.

Padrões relacionados

O Memento interage com padrões de comportamento que também lidam com histórico de ações e gerenciamento de estado:

O Command é o complemento natural e mais frequente do Memento. A distinção crítica: Command desfaz uma ação específica de forma incremental — cada Command sabe exatamente como reverter o que fez. Memento restaura um snapshot completo do estado — não desfaz uma ação, retorna o objeto a um ponto anterior no tempo. Os dois são complementares: um Command pode usar originator.salvarEstado() em seu execute() e originator.restaurarEstado(snapshot) em seu undo(), delegando ao próprio Originator a responsabilidade de se serializar — sem que o Command precise conhecer os campos internos do Receiver. O State também gerencia o estado de um objeto, mas seu foco é representar estados discretos com transições explícitas, não capturar e restaurar estados arbitrários. Memento pode complementar o State quando é necessário retornar a um estado anterior após uma transição — o Memento guarda o snapshot do StateContext antes da transição.