[Delphi] Design Patterns – Memento

[Delphi] Design Patterns - Memento

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.

 

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 backup 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? 🙂

Aba History do editor de códigos exibindo os backups do arquivo

 

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:

Editor de textos para exemplificar a aplicação do Design Pattern Memento

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.

 

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”:

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.

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.

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;

 

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, pensei em utilizar a 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.

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.
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:

type
  TfFormulario = class(TForm)
  ...
  private
    FCaretaker: TCaretaker;
    FOriginator: TOriginator;  
  end;

Para isso, claro, devemos criá-los no construtor, bem como destruí-los no destrutor:

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.

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:

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:

Exemplo de editor de textos utilizando o Design Pattern Memento

 

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:

https://github.com/AndreLuisCelestino/Delphi-DesignPatterns/tree/master/Memento-AndreCelestino

 

Agradeço pela atenção, leitores!
Até a próxima!


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

8 comentários

  1. Boa noite André,

    Top isso hein… show de bola, parabéns… e muito obrigado por contribuir com seus conhecimentos.

    Abraço.

  2. 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!

    1. 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!

  3. 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?

    1. Ó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!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.