C# -  Padrão Comportamental Gof - Memento


 Neste artigo vou apresentar o padrão comportamental Gof Memento.

Segundo a definição da Gang Of Four(GoF), o padrão Memento tem a intenção de capturar e externalizar um estado interno de um objeto, sem violar o encapsulamento, de modo que o mesmo possa posteriormente ser restaurado para este estado.

A palavra Memento traduzida significa lembrança, recordação e esta relacionada com a lembrança de eventos passados.

Ao usar este padrão, você pode restaurar um objeto ao seu estado anterior e assim ele fornece uma maneira orientada a objetos de salvar o estado de um objeto, ou seja,  ele permite salvar e restaurar o estado de um objeto sem quebrar as regras de encapsulamento definidas.

A criação de um ponto de restauração no Windows a partir do qual pode recuperar os dados se houver algum problema é um exemplo de como o padrão Memento atua.  Outro exemplo seria o uso do CTRL+Z para restaurar um texto em um editor de texto.



Desta forma este padrão permite tirar um snapshot ou “retratodo estado de um objeto e a seguir restaurá-lo no futuro.  Isso significa que se você deseja realizar algum tipo de operação de desfazer ou rollback em seu aplicativo, pode usar o padrão Memento para isso.

Uma forma muito usada na linguagem C# de aplicar o principio do padrão Memento é usar a serialização.

Memento : Exemplo

Vejamos um exemplo para entender melhor o padrão Memento.

Vamos supor que temos um objeto que representa uma instância de uma classe Estudante.  Este objeto possui os atributos Id,Nome, Idade, Curso, Nota e Telefone , dentre outros com valores atribuídos.

Podemos dizer que esse é o estado atual do objeto ou seja o estado 1 :



Suponha que em um dado momento realizamos alterações nos valores das propriedades Idade e Nota para esta instância do objeto Estudante atribuindo novos valores a idade e nota :



Assim podemos dizer que alteramos o estado do objeto que agora esta representando pelo estado 2.

Se posteriormente precisarmos desfazer ou reverter as informações do estudante para o estado anterior, ou seja, se precisarmos reverter do estado 2 para o estado 1, podemos usar o padrão Memento.

E como ele atua ?

Este padrão atua armazenando o estado original do objeto em um objeto externo chamado Memento,e ,como esse objeto esta disponível apenas para o objeto cujo estado armazenamos, o padrão respeita os princípios do encapsulamento,  e assim podemos restaurar o estado original do objeto.


É neste cenário que podemos usar é assim que atua o padrão Memento.

Diagrama UML

O diagrama UML do padrão Memento segundo o Gof apresenta os seguintes participantes :


1- Originator

- É o objeto a ser salvo ou armazenado;
- Cria um Memento contendo um snapshot do estado interno atual;
- Usa o Memento para restaurar este estado interno;

Esta classe possui dois métodos :

a - CreateMemento - Cria uma instancia de Memento;
b - SetMemento - retorna o estado salvo e atribui o valor ; este método aceita o objeto memento;

Esta classe instancia o objeto Memento e define o estado interno do Originator para o estado memento, ou seja, ela tira um instantâneo do Originator e o coloca no objeto memento. Este objeto memento será salvo no Caretaker para uso posterior caso isso for preciso.

2- Memento

- Representa um estado armazenado;
- Armazena o estado interno do objeto Originator;

Esta classe protege contra o estado do acesso de outros objetos que não o Originator.  Ela contém dois métodos :

a- SetState - usado para armazenar as informações do estado;
b- GetState - usado para recuperar o estado salvo;

3- CareTaker

- Responsável por persistir o Memento;
- Fornece o Memento de volta ao Originator para restaurar o estado interno;

Esta classe apenas armazena o estado, ela nunca modifica ou examina o conteúdo do objeto Memento. Aqui devemos tomar cuidado para não expor o estado do objeto ao mundo exterior.

Quando podemos usar o padrão

Podemos usar o padrão Memento nos seguintes cenários :

- O estado de um objeto precisa ser salvo e restaurado posteriormente (operações de desfazer , rollback, etc. );

- O estado de um objeto não pode ser exposto diretamente usando uma interface sem expor a implementação;

Assim este padrão é recomendado em qualquer aplicação em que o estado do objeto muda continuamente e o usuário da aplicação pode decidir reverter ou desfazer as mudanças a qualquer momento.

Vantagens do padrão

Como vantagens deste padrão temos que :

- Permite armazenar o estado dos objetos sem comprometer o encapsulamento;
- Fornece um mecanismo de recuperação em caso de falhas;
- Fornece uma maneira de manter o histórico do ciclo de vida de um objeto;

Desvantagem

E como desvantagem podemos citar que :

O processo de salvar o estado de um objeto e restaurá-lo mais tarde pode levar algum tempo, ou seja, pode ser prejudicial ao desempenho do aplicativo.

A aplicação pode consumir uma grande quantidade de RAM se o usuário criar muitos pontos de restauração (Memento), e,  o tempo extra para salvar os estados irá reduzir o desempenho geral do aplicativo.

Assim, dependendo da quantidade de informações de estado que deve ser armazenada dentro do objeto memento o processo de salvar e restaurar o estado pode impactar o desempenho. Além disso, a classe Caretaker vai precisar conter lógica adicional para ser capaz de gerenciar o Memento o que pode afetar a aplicação.

Exemplo de implementação do padrão Memento

Como exemplo de aplicação do padrão vamos implementar o padrão Memento para recuperar o estado de uma operação realizada com dois números inteiros.

Assim vamos poder realizar uma operação e ter um resultado:



Depois vamos realizar outra operação, e, a seguir vamos recuperar o resultado da primeira operação :



Implementação prática

Levando em conta este cenário vamos implementar o padrão Memento usando uma aplicação Console .NET Core (.NET 5.0) criada no VS 2019 Community.

A seguir temos o diagrama de classes obtido a partir do VS 2019 na implementação do padrão:

Podemos identificar as seguinte classes :

- ICalculadora - Define a interface para a implementação do Originator;

- Calculadora -
 Implementa a interface ICalculadora e restaura o estado anterior. Representa o Originator;

- IOriginator -   Permite o Originator restaurar o seu estado;

- Memento -  Armazena o estado do objeto Originator. Só temos Get não temos como alterar os dados;

- ICareTake -  Não definimos nenhuma operação nesta interface. (Poderíamos ter definido a data e o nome);

A seguir temos o código usado na implementação:

1- A interface ICalculadora

    public interface ICalculadora
    {
        //CreateMemento
        ICaretaker BackupUltimoCalculo();

        //SetMemento
        void RestauraUltimoCalculo(ICaretaker memento);

        //servicos do Originator
        int GetCalculoResultado();
        void SetPrimeiroNumero(int num1);
        void SetSegundoNumero(int num2);
    }
}

2- A Interface IOriginator

     public interface IOriginator
    {
        int GetPrimeiroNumero();
        int GetSegundoNumero();
    }

3- Classe Calculadora

     public class Calculadora : ICalculadora
    {
        private int primeiroNumero;
        private int segundoNumero;

        //corresponde ao método CreateMemento()
        public ICaretaker BackupUltimoCalculo()
        {
            // cria um objeto memento usado para restaurar os dois numeros
            return new Memento(primeiroNumero, segundoNumero);
        }
        public int GetCalculoResultado()
        {
            // retorna o resultado da soma dos dois numeros
            return primeiroNumero + segundoNumero;
        }
        // Corresponde ao método SetMemento()
        // Executa a lógica para restaurar o estado anterior

        public void RestauraUltimoCalculo(ICaretaker memento)
        {
            primeiroNumero = ((IOriginator)memento).GetPrimeiroNumero();
            segundoNumero = ((IOriginator)memento).GetSegundoNumero();
        }
        public void SetPrimeiroNumero(int num1)
        {
            primeiroNumero = num1;
        }
        public void SetSegundoNumero(int num2)
        {
            segundoNumero = num2;
        }
    }

4- Classe Memento

    public class Memento : ICaretaker, IOriginator
    {
        private int primeiroNumero;
        private int segundoNumero;
        public Memento(int numero1, int numero2)
        {
            primeiroNumero = numero1;
            segundoNumero = numero2;
        }
        public int GetPrimeiroNumero()
        {
            return primeiroNumero;
        }
        public int GetSegundoNumero()
        {
            return segundoNumero;
        }
    }

7- Program

using System;

namespace Memento2
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("###  Padrão Memento ###\n");
            Console.WriteLine("Calcula a soma de dois numeros e armazena o estado\n");
            Console.WriteLine("Pressione algo para realizar outro calculo e restaurar o estado anterior\n");

// cria instancia do tipo ICalculadora
ICalculadora calculadora = new Calculadora();

// Atribui dois numeros
calculadora.SetPrimeiroNumero(10);
calculadora.SetSegundoNumero(100);

            // retorna o resultado
            Console.WriteLine($"### Estado 1 : {calculadora.GetCalculoResultado()}");

// Armazena o resultado caso ocorra um erro
ICaretaker memento = calculadora.BackupUltimoCalculo();
Console.ReadKey();

            Console.WriteLine("\nCalcula uma nova soma de dois numeros \n");

// atribui outro numero
calculadora.SetPrimeiroNumero(17);

// atribui um segundo numero incorreto e calcula
calculadora.SetSegundoNumero(-290);

// exibe o resultado
Console.WriteLine($"Estado 2 : {calculadora.GetCalculoResultado()}");

Console.WriteLine("\nRestaura o resultado da soma anterior\n");

// Realização operação para desfazer a ultima operação (CTRL+Z , Undo)
calculadora.RestauraUltimoCalculo(memento);

            // Restaura o resultado do estado armazenado
            Console.WriteLine($"### Estado 1 : {calculadora.GetCalculoResultado()}");

Console.ReadKey();
          }
    }
}

A execução do projeto irá apresentar o seguinte resultado:

Dessa forma o padrão Memento nos dá uma maneira prática de armazenar e recuperar o estado de um objeto.  Você pode aproveitar esse padrão para realizar operações para desfazer ou reverter o estado original.

Ao usar o padrão Memento, certifique-se de manter o desempenho em mente, tendo o cuidado de não expor ao mundo externo a estrutura interna do seu objeto. 

Este padrão esta relacionado com o padrão Command que pode usar o Memento para manter o estado para as operações que podem ser desfeitas.

Pegue o código do projeto aqui :
  Memento1.zip

"Ai dos que ao mal chamam bem, e ao bem mal; que fazem das trevas luz, e da luz trevas; e fazem do amargo doce, e do doce amargo!"
Isaías 5:20

Referências:


José Carlos Macoratti