Saudações, caros leitores!
Como bons programadores, sabemos que, em muitas ocasiões, é preciso salvar o estado atual de um objeto ou entidade antes de realizar uma operação, de forma que possamos restaurá-lo caso necessário. Um bom exemplo é o controle de transação de banco de dados – quando um erro ocorre, executamos um rollback na operação, restaurando o estado anterior de uma tabela. Esse procedimento de armazenamento e restauração de estados é basicamente o objetivo do Design Pattern deste artigo, chamado Memento.
Introdução
Ao pesquisar por “Memento” no Google, os primeiros resultados trazem informações de um filme de 2000, cujo tradução, em português, é “Amnésia”. O padrão de projeto Memento tem relação com esse significado. Quando alteramos o estado interno de um objeto (como os seus atributos), “perdemos” os valores armazenados anteriormente. No contexto deste artigo, podemos dizer que “esquecemos” dos valores anteriores. Daí a Amnésia.
Porém, a proposta do Memento é justamente o inverso: permitir que a aplicação se lembre dos estados anteriores de um objeto. Quando digo “lembrar”, me refiro especificamente à possibilidade de restaurar um estado anterior. Em termos mais técnicos, compare o Memento com a operação de desfazer (Ctrl + Z), utilizada com bastante frequência no dia-a-dia. Ao desfazer uma ação, o sistema restaura o estado anterior dos dados, ou seja, antes da última alteração.
O rollback de uma transação no banco de dados, mencionado no enunciado do artigo, também é um exemplo tradicional de restauração de um estado anterior que, neste caso, são os dados de tabelas.
Para complementar os exemplos, eu diria que o próprio Delphi utiliza uma implementação de Memento na funcionalidade de histórico dos arquivos. Cada vez que um arquivo de código é salvo com uma alteração, um novo backup é criado no subdiretório __history
na pasta do projeto. Esse histórico de alterações pode ser visualizado na aba History do editor de códigos, permitindo que um estado anterior do arquivo seja restaurado. Agora ficou claro, não? 🙂
Exemplo de codificação do Memento
A compreensão do padrão de projeto ficará ainda mais fácil com um cenário prático.
Considere um editor de textos, bem simples, que contém um TEdit
para o título, um TRichEdit
para o texto e uma barra de ferramentas para formatação, exemplificado na imagem abaixo:
Do lado esquerdo do formulário, em um TListBox
, armazenaremos o histórico das alterações do texto, no qual será salvo a cada ação do botão “Salvar Alterações”. Por meio deste histórico, poderemos navegar nos estados anteriores do texto, restaurando-os quando desejado, semelhante à funcionalidade que existe atualmente no WordPress.
Explicarei os elementos do Memento conforme avançamos nas codificações.
Classe Memento
O primeiro passo é modelar a classe que leva o próprio nome do padrão de projeto: Memento. Essa classe é responsável por armazenar os atributos do objeto que poderá ser restaurado. Sendo assim, cada instância dessa classe será um estado armazenado, ou, tecnicamente falando, um snapshot do objeto naquele momento. Para o nosso exemplo, haverá apenas dois atributos, chamados “Titulo” e “Texto”:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
type { Memento } TMemento = class private FTitulo: string; FTexto: WideString; public // Propriedade referente ao título do texto property Titulo: string read FTitulo write FTitulo; // Propriedade referente ao corpo do texto property Texto: WideString read FTexto write FTexto; end; |
André, não devemos salvar também as formatações do texto?
Não, não precisa. Como bônus, apresentarei uma forma de extrair as formatações do TRichEdit
como string. Por este motivo que a propriedade Texto
foi declarada como WideString
.
Classe Originator
O segundo passo é criar a classe Originator que, como o próprio nome infere, tem a função de “originar” os estados do objeto. Além disso, o a classe também disponibiliza um método para salvar o estado atual (título e texto com as formatações) e restaurar um estado anterior (sobrescrevendo o título e o texto existente).
O primeiro método cria uma instância do Memento, preenche e o retorna ao chamador. O segundo recebe um Memento como parâmetro para consumir os seus atributos. Podemos afirmar, portanto, que a instância do Originator sempre representará os dados vigentes do objeto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
type TOriginator = class private FTitulo: string; FTexto: WideString; public // Propriedade referente ao título do texto property Titulo: string read FTitulo write FTitulo; // Propriedade referente ao corpo do texto property Texto: WideString read FTexto write FTexto; // Função que cria, preenche e retorna um Memento function SalvarEstado: TMemento; // Método que usa o Memento informado no parâmetro para restaurar o estado procedure RestaurarEstado(Memento: TMemento); end; implementation { TOriginator } function TOriginator.SalvarEstado: TMemento; begin // Cria uma instância do Memento result := TMemento.Create; // Preenche o objeto result.Titulo := FTitulo; result.Texto := FTexto; end; procedure TOriginator.RestaurarEstado(Memento: TMemento); begin // Sobrescreve as propriedades com os dados do Memento do parâmetro FTitulo := Memento.Titulo; FTexto := Memento.Texto; end; |
Classe Caretaker
Algo está faltando, não acha? Precisamos de uma classe que contenha uma lista dos estados do objeto para coordenar os armazenamentos e as restaurações. Essa é responsabilidade do elemento Caretaker, que significa “zelador” em português. O Caretaker possui métodos para adicionar um novo estado e obter um estado já existente, lembrando que, quando menciono “estado”, me refiro à uma instância do Memento.
Para armazenar a lista de Mementos, considerei o uso da classe TObjectDictionary
, do namespace System.Generics.Collections
. Essa classe é bem similar ao TDictionary
tradicional, porém, é exclusiva para armazenar objetos como valores. No construtor do TObjectDictionary
, podemos indicar que o próprio dicionário será responsável por liberar os objetos armazenados, informando o valor [doOwnsValues]
.
Por fim, trabalharemos com uma combinação de data e hora como chave do dicionário. O objetivo é gravar um snapshot do texto na data e hora em que o usuário clicou no botão “Salvar Alterações”. Isso nos permitirá visualizar e identificar as alterações com mais facilidade.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
type { Caretaker } TCaretaker = class private FHistoricoAlteracoes: TObjectDictionary<string, TMemento>; public constructor Create; destructor Destroy; override; // Método para adicionar um novo Memento na lista procedure Adicionar(const DataHora: string; Memento: TMemento); // Função para retornar um Memento conforme a data e hora function Obter(const DataHora: string): TMemento; end; implementation { TCaretaker } constructor TCaretaker.Create; begin // Cria o dicionário de objetos // "doOwnsValues" significa que o próprio dicionário irá liberar os objetos internos FHistoricoAlteracoes := TObjectDictionary<string, TMemento>.Create([doOwnsValues]); end; destructor TCaretaker.Destroy; begin // Libera o dicionário de objetos da memória FHistoricoAlteracoes.Free; inherited; end; procedure TCaretaker.Adicionar(const DataHora: string; Memento: TMemento); begin // Adiciona o Memento no dicionário de objetos FHistoricoAlteracoes.Add(DataHora, Memento); end; function TCaretaker.Obter(const DataHora: string): TMemento; begin // Obtém o memento conforme a chave, que é uma combinação da data e hora result := FHistoricoAlteracoes.Items[DataHora]; end; |
Bom, feito isso, a codificação do padrão de projeto está concluída! O último passo é consumir toda essa arquitetura.
Em ação!
No formulário (editor de textos), teremos duas variáveis de classe: uma para o Originator, que irá refletir o estado atual do texto; e outra para o Caretaker, responsável por controlar os Mementos:
1 2 3 4 5 6 7 |
type TfFormulario = class(TForm) ... private FCaretaker: TCaretaker; FOriginator: TOriginator; end; |
Para isso, claro, devemos criá-los no construtor, bem como destruí-los no destrutor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure TfFormulario.FormCreate(Sender: TObject); begin // Cria o Caretaker (que contém o dicionário de objetos) FCaretaker := TCaretaker.Create; // Cria o Originator, que corresponde aos dados atuais apresentados na tela FOriginator := TOriginator.Create; end; procedure TfFormulario.FormDestroy(Sender: TObject); begin // Libera as variáveis de classe da memória FOriginator.Free; FCaretaker.Free; end; |
Em seguida, acompanhem a codificação do botão “Salvar Alterações” e atentem-se aos comentários. Nesse método, conforme prometi que apresentaria no artigo, faço uso da classe TStringStream
para obter o texto formatado do TRichEdit
como string por meio da propriedade DataString
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
var DataHoraAtual: string; Texto: TStringStream; begin // Preenche a informação de título do Originator FOriginator.Titulo := EditTitulo.Text; // Cria uma instância da classe TStringStream Texto := TStringStream.Create; try // Salva o texto formatado do TRichEdit na stream RichEditTexto.Lines.SaveToStream(Texto); // Converte a stream em string, preenchendo a informação de texto do Originator FOriginator.Texto := Texto.DataString; finally // Libera a instância de TStringStream Texto.Free; end; // Formata a data e hora atual, transformando-as em uma string DataHoraAtual := FormatDateTime('dd/mm/yyyy hh:nn:ss:zzz', Now); // Adiciona o novo estado (Memento) no dicionário FCaretaker.Adicionar(DataHoraAtual, FOriginator.SalvarEstado); // Adiciona a alteração no histórico ListBoxHistoricoAlteracoes.Items.Add(DataHoraAtual); end; |
Sem muito segredo, não é? Vejam que, após preencher os atributos do Originator, adicionamos um instantâneo do seu estado na lista do Caretaker através do método SalvarEstado
. Lembram-se do que ele faz? Cria, preenche e retorna uma instância da classe Memento. 🙂
Cada vez que o botão “Salvar Alterações” for acionado, um novo item será adicionado na TListBox
, indicando que um novo estado (Memento) foi armazenado na lista do Caretaker.
Imagine, agora, que o usuário aplicou algumas alterações incorretas no texto e deseja restaurar um estado anterior. O procedimento é simples. Codificaremos o evento OnClick
da TListBox
para buscar o estado (Memento) referente àquela data e hora selecionada, atribuindo suas propriedades ao Originator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
var DataHora: string; Texto: TStringStream; begin DataHora := ListBoxHistoricoAlteracoes.Items[ListBoxHistoricoAlteracoes.ItemIndex]; // Obtém o estado conforme o item selecionado no histórico de alterações, // e restaura o título e o texto (substituindo os dados do Originator pelos dados do Memento encontrado) FOriginator.RestaurarEstado(FCaretaker.Obter(DataHora)); // Preenche o campo referente ao título com os dados do Originator EditTitulo.Text := FOriginator.Titulo; // Cria uma instância da classe TStringStream informando o texto como parâmetro Texto := TStringStream.Create(FOriginator.Texto); try // Preenche o campo referente ao texto, carregando-o da stream RichEditTexto.Lines.LoadFromStream(Texto); finally // Libera a instância de TStringStream Texto.Free; end; end; |
Na prática, este será o resultado:
Conclusão
Antes de finalizar o artigo, uma observação: no formulário (Client), não foi necessário adicionar a unit da classe Memento na seção uses, apenas do Originator e do Caretaker. Lembrem-se: baixo acoplamento!
Utilize o Memento em funcionalidades que exijam a recuperação de dados anteriores de um objeto específico. Uma ideia é utilizá-lo no preview de telas ou personalização de relatórios, permitindo que o usuário desfaça as alterações de forma rápida e confiável.
O projeto de exemplo deste artigo está disponível no link abaixo:
Agradeço pela atenção, leitores!
Até a próxima!
Boa noite André,
Top isso hein… show de bola, parabéns… e muito obrigado por contribuir com seus conhecimentos.
Abraço.
Olá, Daniel!
Eu que agradeço por sempre acompanhar, comentar e divulgar o blog.
Grande abraço!
Olá André!
Excelente artigo, como de costume. Parabéns pela clareza nas explicações sempre com exemplos muito explicativos, ricos em detalhes. Dessa vez abordando um dos design patterns menos difundidos que é o Memento. Com certeza a sua série de artigos é um ótimo material para quem, assim como eu, procura manter a qualidade do código.
Continue assim, guri! Abração!
Agradeço fortemente pelo comentário, Cleo! E também aproveito para agradecê-lo por acompanhar essa série de artigos!
Este trabalho de estudar, escrever e divulgar os Design Patterns no blog tem sido bastante prazeroso. Ainda faltam 5 pela frente!
Em breve, o Observer! 🙂
Grande abraço, Cleo!
Buenas André, a forma que você apresenta os Design Patterns são bem esclarecedoras. Parabéns!
Queria saber sua opinião sobre manter o estado atual do Memento em TOriginator, no exemplo as propriedades estão repetidas, eu poderia por exemplo, deixar declarado dentro de TOriginator um MementoAtual: TMemento?
Ótima observação, Giquieu!
Quando eu estudei sobre o padrão de projeto, também fiquei um pouco perplexo com essa repetição de atributos no Memento e Originator.
Acredito que a sua ideia é bastante válida! Ao invés de declarar atributos repetidos, utilizar um Memento dentro do Originator para representar os dados vigentes. Excelente!
Obrigado pelo comentário. Abraço!
Parabéns André por mais um excelente artigo.
Muito obrigado, Franklin!
Continue acompanhando! Abraço!