Padrão 17 · Comportamentais
Memento
guardar tokens opacos de capturas e restaurar o estado declarado de um diário.
Começar pelo código
01 · Entenda a escolha
Os fundamentos do padrão.
17 — Memento: O Guardião da Memória Perdida
Lia pode restaurar o diário a uma captura anterior. Isso substitui o estado atual do diário, inclusive edições posteriores que ela talvez queira preservar. O programa copia uma anotação atual separadamente antes de restaurar. Recuperar uma frase não localiza ou resgata uma pessoa e não restaura o mundo.
A alternativa mais simples
Para um único valor que o mesmo código pode ler, guardar a string anterior numa variável e atribuí-la novamente é suficiente. src/main.ts começa executando essa alternativa. Não há razão para esconder o valor em tokens quando não existe uma separação de responsabilidades que justifique isso.
Aqui o problema adicional é permitir que o guardião armazene e selecione capturas sem receber o conteúdo capturado. Diario é o Originator; os tokens opacos são os Mementos; GuardiaoDasMemorias é o Caretaker. O guardião guarda tokens e rótulos, sem analisar ou restaurar o estado por conta própria. O cliente entrega o token selecionado ao próprio diário, que sabe interpretá-lo.
Executar
Versões declaradas e utilizadas: Node 26.10.0, npm 11.19.1, TypeScript 5.9.3, @types/node 26.0.0. Há .nvmrc, lock próprio e configuração estrita ESM NodeNext; não há importação de outro projeto.
Na pasta book-23/code/17-memento:
npm ci --ignore-scripts --no-audit --no-fund
npm test
npm start
npm run solution
Na raiz do repositório:
node book-23/scripts/projetos.mjs 17 install
node book-23/scripts/projetos.mjs 17 check
node book-23/scripts/projetos.mjs 17 run
node book-23/scripts/projetos.mjs 17 solution
node book-23/scripts/projetos.mjs 17 capture
node book-23/scripts/projetos.mjs 17 verify
evidence/manifest.json vincula fontes, versões, relatório de testes e as duas saídas funcionais por SHA256. stdout.txt e solution-stdout.txt vêm dos programas reais, sem banners npm. runner-commands.json registra os seis comandos do runner. isolation.json registra uma cópia temporária contendo somente este projeto, com instalação, testes, programas e comparação byte a byte das duas saídas. Tempos de teste variam; não são um benchmark. Evidências de execução do produtor não atribuem aprovação humana nem auditoria independente.
Estado, captura e restauração
O estado declarado é EstadoDiario: conteudo é string; contexto contém local string e marcadores, uma lista de strings. substituir(estado) substitui o documento ativo completo. ler() devolve uma cópia congelada do estado ativo, sem receber token ou revelar uma captura histórica.
Entrada, captura, leitura e restauração copiam explicitamente os campos do modelo, incluindo objeto aninhado e lista. As cópias e suas estruturas declaradas são congeladas. Mutar a entrada original não altera o documento nem a captura. Restaurar, editar novamente e restaurar o mesmo token volta ao estado capturado, sem contaminar a memória. Não há cópia profunda genérica de classes, funções, recursos externos ou grafos cíclicos. O contrato de entrada é esse modelo de dados; TypeScript não valida todo objeto arbitrário vindo de JavaScript sem tipos.
salvar() cria um objeto vazio, congelado e de protótipo nulo. O token não tem getter, campos ou símbolos que contenham o estado. O nominal Memoria ajuda na compilação; a validação real é a presença da mesma identidade no WeakMap privado #capturas daquela instância de diário. O payload fica nesse mapa, não no token. restaurar(token) recupera e copia esse payload. Token falso, cópia de campos, resultado de JSON e token de outro diário são recusados com erro, sem alterar o estado ativo.
Esse encapsulamento corrige o getter público de estado da fonte autoral. Não se promete segurança contra código hostil no mesmo processo, alteração de protótipos/dependências, depuração privilegiada ou vazamento da referência ao próprio diário. Quem recebe Diario pode usar seu método público de leitura ativa. Tokens também conferem a quem os possui a possibilidade de pedir restauração ao diário correto; opacidade não equivale a autorização de pessoas.
Restaurar substitui todo o estado declarado e perde intencionalmente as edições posteriores no documento ativo. A cópia separada de main.ts é uma decisão do cliente anterior à restauração, não um resgate automático de todo trabalho perdido. O token não captura arquivos, papel, pessoas ou sistemas externos. Nenhum efeito externo é desfeito.
Guardião, índice e limite
GuardiaoDasMemorias(limite) exige inteiro seguro positivo. adicionar(token, rotulo) guarda uma referência ao token e um rótulo textual não vazio. listar() só expõe índices e rótulos; obter(indice) entrega o token para seleção. O índice é zero-based, inteiro e dentro da lista atual; negativos, frações, NaN, infinito e posições inexistentes lançam erro. As listagens são novas listas de itens congelados.
Ao exceder o limite, o guardião descarta a entrada mais antiga e os índices restantes são renumerados. Isso não restaura o diário nem revoga tokens que outro código tenha conservado. O guardião não valida o payload ou a origem de tokens: essa verificação pertence ao Originator ao restaurar. Os rótulos não contêm texto do documento automaticamente; quem os escolhe deve evitar colocar neles conteúdo que pretendia manter privado.
Wrapper e política de checkpoints do exercício
portaMemorias() devolve um objeto separado real, congelado, com apenas salvar, restaurar e correspondeCheckpoint. Não é o próprio diário convertido para uma interface estreita; métodos ler e substituir não estão presentes nesse wrapper em execução. As closures mantêm a referência ao diário sem devolvê-la como campo. Isso reduz a API entregue ao caretaker do exercício, sem prometer proteção hostil geral.
correspondeCheckpoint(token) verifica igualdade da marca privada de versão, não igualdade profunda de conteúdo. substituir() cria uma nova marca, mesmo se os valores escritos forem iguais aos anteriores. Restaurar um token recupera sua marca. O booleano permite reconhecer uma edição pendente sem expor texto, local ou marcadores. Token inválido continua sendo erro.
A base demonstra seleção direta de memória: restaurar por escolha do cliente pode perder uma edição não capturada. O exercício LinhaDoTempo acrescenta uma política mais restrita, explicada em exercise/README.md: captura inicial automática, confirmação explícita depois de editar, recusa de navegar com edição pendente, undo/redo e invalidação da ramificação futura na confirmação de nova edição após undo. Essa política não é uma propriedade automática de Memento.
Custos e comparação
Cada cópia percorre os campos declarados e a lista de marcadores, além das alocações correspondentes; textos são valores de string, sem promessa de uma cópia física de bytes a cada atribuição. Memórias conservam o payload das versões capturadas enquanto seus tokens forem alcançáveis. O histórico limitado reduz as referências guardadas pelo caretaker, mas não limita referências retidas por outros clientes. WeakMap não fornece um prazo exato de coleta de lixo. O descarte com shift() pode deslocar entradas, e as listagens copiam metadados.
Estados grandes ou muitas capturas podem tornar esse custo inadequado. Uma operação inversa pequena pode ser mais econômica quando seu contrato é seguro. Command encapsula uma solicitação e pode guardar dados para uma inversa específica; Memento conserva estado para restauração pelo Originator. Restaurar este diário inteiro deliberadamente substitui edições posteriores, enquanto cancelar uma reserva específica pode preservar outras operações. Nenhuma das duas intenções garante reversibilidade física ou transações universais.
Serialização é transformação de dados para outra representação e requer um contrato próprio de formato/versão. Aqui JSON.stringify(token) resulta em {}; desserializar isso não recria um token válido ou uma captura. Este projeto não exporta snapshots persistentes e não promete que JSON ou structuredClone restaurem identidade, métodos, recursos ou encapsulamento.
Arquivos
src/diario.ts: Originator, estado, tokens e wrapper. src/guardiao.ts: armazenamento/seleção e validação de índice/limite. src/main.ts: alternativa direta, cópia separada e restauração. test/memento.test.ts: opacidade em execução, cópias, origem, restauração, limite e wrapper. diagram.mmd: somente relações da base. solution/linha-do-tempo.ts e testes: política adicional de undo/redo, sem alterar a base para acomodar o exercício.
02 · Leia o mecanismo
O feitiço, linha por linha.
Comece por src/main.ts para ver o programa em uso. Explore os contratos, a implementação e os testes no seletor de arquivos.
import { Diario } from "./diario.js";
import { GuardiaoDasMemorias } from "./guardiao.js";
// Alternativa simples, suficiente para um único valor sem encapsulamento entre papéis.
let valorAtivo = "Anotação simples inicial.";
const valorGuardado = valorAtivo;
valorAtivo = "Rascunho posterior.";
const antesDaCopiaDireta = valorAtivo;
valorAtivo = valorGuardado;
console.log(`Valor direto: ${antesDaCopiaDireta} -> ${valorAtivo}`);
const diario = new Diario({ conteudo: "A testemunha indicou o arquivo norte.",
contexto: { local: "Chronoria", marcadores: ["depoimento"] } });
const guardiao = new GuardiaoDasMemorias(2);
guardiao.adicionar(diario.salvar(), "Depoimento inicial");
diario.substituir({ conteudo: "Anotação pessoal posterior.", contexto: { local: "Sala do diário", marcadores: ["pessoal"] } });
const copiaSeparada = diario.ler();
console.log(`Antes: ${diario.ler().conteudo}`);
console.log(`Guardião: ${guardiao.listar()[0]!.rotulo}`);
console.log(`Campos no token: ${Reflect.ownKeys(guardiao.obter(0)).length}`);
diario.restaurar(guardiao.obter(0));
console.log(`Restaurado: ${diario.ler().conteudo}`);
console.log(`Contexto restaurado: ${diario.ler().contexto.local} | ${diario.ler().contexto.marcadores.join(", ")}`);
console.log(`Cópia separada: ${copiaSeparada.conteudo}`);
03 · Revele as conexões
A estrutura por trás da magia.
Veja os papéis e suas relações. Amplie cada vista para ler atributos, métodos e conexões com calma.
Esta vista reúne Diario, Memoria. Diario depende de Memoria (cria e reconhece identidade). Membros exibidos são um recorte; construtores e demais membros podem estar omitidos. e1 · cria e reconhece identidade
Esta vista reúne Memoria, GuardiaoDasMemorias. GuardiaoDasMemorias mantém referência a Memoria (armazena e seleciona). Membros exibidos são um recorte; construtores e demais membros podem estar omitidos. e2 · armazena e seleciona
Esta vista reúne Diario, PortaMemorias. Diario depende de PortaMemorias (cria wrapper real separado). Membros exibidos são um recorte; construtores e demais membros podem estar omitidos. e3 · cria wrapper real separado
Esta vista reúne Diario, Memoria, GuardiaoDasMemorias, PortaMemorias. Memoria: Vazio, congelado, protótipo nulo. Payload somente no WeakMap privado da instância de Diario. Diario: Leitura pública consulta o estado ativo, nunca um token. Restaurar substitui edições posteriores. GuardiaoDasMemorias: Não recebe Diario nem lê payload. Metadados: índice e rótulo. PortaMemorias: Wrapper sem ler/substituir em execução. Booleano compara marca de versão, não igualdade profunda. Membros exibidos são um recorte; construtores e demais membros podem estar omitidos.
04 · Faça sua escolha
Sua vez de conjurar.
Exercício — Undo/redo com confirmação explícita
Implemente LinhaDoTempo recebendo a porta estreita real de memórias, sem receber Diario com seus métodos de leitura e edição. O histórico deve ser limitado, e uma edição posterior a undo deve abrir nova ramificação quando confirmada.
Critérios verificáveis:
- Na construção, capture automaticamente o estado atual como checkpoint
Inicial, com cursor zero. Esse ponto conta no limite. - O cliente edita o diário e chama
confirmar(rotulo). Só essa confirmação captura a edição como ponto navegável. Sem nova edição, confirmar retornafalsee não duplica um ponto. - Undo e redo recusam navegar se houver edição pendente, inclusive nas bordas; não descartam texto silenciosamente. O usuário precisa confirmar antes de navegar. Não há descarte implícito ou comando adicional de descartar nesta solução.
- Sem pendência, undo/redo restauram o checkpoint vizinho, movem cursor só depois do sucesso e retornam
falsena borda correspondente. - Uma edição feita após undo torna a navegação pendente; confirmá-la corta os checkpoints à frente e cria o novo futuro. Antes disso, os metadados da ramificação antiga ainda existem, embora redo esteja bloqueado pela pendência.
- O limite inclui o ponto atual. Excedê-lo remove o mais antigo e reajusta índices/cursor. Limite um mantém somente o atual e não permite undo.
Resposta comentada
solution/linha-do-tempo.ts mantém apenas tokens, rótulos e cursor. O estado atual entra na linha do tempo no construtor e depois por confirmações explícitas; não é capturado automaticamente quando se clica em undo. Isso evita fingir que todo texto ativo já estava salvo.
O booleano correspondeCheckpoint consulta a marca de versão privada do Originator, sem acesso ao payload. Ele não compara textos: uma edição com valores iguais também cria uma nova versão e exige confirmação. confirmar valida o rótulo e cria o token antes de cortar o futuro; undo/redo restauram antes de mover cursor. As garantias valem para a porta fornecida e operações síncronas deste modelo; uma implementação externa com efeitos ou falhas arbitrárias tem seus próprios limites.
A base já continha operações de captura/restauração, marca de versão e wrapper necessárias a esse recorte. A solução acrescenta o controlador de checkpoints e seus testes, sem instalar getter bruto de estado nem alterar a base. Não há persistência, múltiplos editores concorrentes, deduplicação por conteúdo ou recuperação do futuro eliminado pelo controlador. Tokens mantidos externamente não são revogados pelo corte.
Execute npm run solution para observar A → B → C, undo para B, redo para C e nova ramificação B → D sem redo para C. npm test verifica estado completo, pendência, limite/cursor, confirmação sem edição e listagens. Uma simples string anterior continua sendo uma alternativa legítima para um único estado sem essa política de histórico.
Compare depois da sua tentativa.
A resposta e seus testes estão na pasta solution/. O sucesso dos testes fornecidos confirma essa resposta; sua tentativa precisa estar incluída na compilação para ser avaliada.
Do estudo à prática
Experimente localmente.
Node 26.10.0 · npm 11.19.1 · TypeScript 5.9.3
Execute os comandos dentro da pasta deste projeto. O lock instala o compilador local. Os programas não são executados no navegador.
cd book-23/code/17-memento npm ci --ignore-scripts --no-audit --no-fund npm test npm start # Depois da sua tentativa npm run solutionBaixar este projeto completo