Patrones de diseño. Memento Pattern.

Introducción

En muchas ocasiones deseamos guardar el estado de un objeto de forma que podamos recuperar dicho estado en cualquier momento. Un ejemplo muy simple es que vayamos a modificar una serie de parametros del objeto y hacer alguna operación pero deseemos almacenar en que estado estaba ese objeto para poder volver a él en caso de que se produzca una excepción.

En la mayoría de los objetos existe una parte de su estado que queda recogida en variables privadas. Al contrario de lo que se suela pensar una campo es privado no para evitar que alguien pueda modificarlo (que tangencialmente también) sino para indicar al programador que usa la clase que dicho campo no le interesa y no debe tenerlo en cuenta, utilizarlo ni depender en ningún sentido. Si cambiamos dichas variables de privadas a publicas, conseguimos que quien quiere guardar el estado pueda hacerlo leyendo y almacenando los campos pero violamos el concepto anterior.

El patrón memento enfrenta este problema encapsulando la información interna del objeto en una objeto, preferiblemente opaco al exterior.

Problema

Proporcionar un medio para almacenar el estado completo de un determinado objeto y poder restaurar dicho estado en cualquier momento. En la mayoría de las ocasiones el estado que queremos guardar no se guardará dentro del propio objeto sino en otro.

Aplicabilidad

El patrón memento se puede utilizar, fundamentalmente, para guardar instantaneas del estado de un objeto permitiendonos la implementación de utilidades de "deshacer" y "rehacer" o incluso almacenar el estado completo de una aplicación.

Estructura

Memento.png

Podemos distinguir en el esquema la presencia de 3 entidades. Por un lado tenemos al Originador, que es la instancia del objeto de la que queremos salvar el estado. Por otro lado tenemos el Consumidor (o cliente) que es la clase que necesita obtener y almacenar ese estado para, en un momento futuro, si procede, restaurarlo. Finalmente el objeto Memento encapsula de forma opaca el estado del originador permitiendo que el consumidor lo almacene y lo utilice para restaurar el estado de este en cualquier momento.

Código de ejemplo y matices al código

Antes de entrar a ver los ejemplos de código concretos cabe resaltar que existen diversas formas de afrontar el patrón. En primer lugar, aunque es deseable que la clase Memento sea opaca, en algunos lenguajes esto no será posible y en otros tendremos que recurrir al uso de RTI o de interfaces. Recordemos que en realidad dicha restricción no es esencial, sino que sencillamente el consumidor no debería preocuparse por el contenido del Memento que obtiene, sino simplemente almacenarlo y pasarlo al originador en su momento.

Voy a repasar unas cuantas aproximaciones para varios lenguajes de programación como C# o Delphi, en general espero que sirvan para mostrar las diversas formas de implementar la opacidad mencionada aunque, como ya he dicho, en su caso más básico bastará con limitarse a encapsular el estado del objeto, cualquiera que sea los datos que incluya en un objeto Memento y recuperar dicho estado mas tarde.

C#

En C# podemos encapsular la funcionalidad mencionada en una instancia de la clase object (que es la clase base de .NET) y, utilizando las propiedades de RTI 1 que nos proporciona .NET mediante reflection obtener la clase adecuada desde nuestro objeto. Así:

public class Originador
{
  private string name;
  private int x;
  private int y;

  public object GetState()
  {
    return new Memento{ Name = this.Name, X = this.x, Y = this.y }
  }

  public void SetState(object memento)
  {
    if (memento.GetType() == typeof(Memento))
    {
       Memento m = memento as Memento;
       this.name = m.Name;
       this.x = m.X;
       this.y = m.Y;
    }
    else
       throw new InvalidMementoTypeException();
  }

  private class Memento
  {
    public string Name { get; set; }
    public int X { get; set;}
    public int Y { get; set;}
  }
}

De esta forma, cualquier objeto que desee obtener una instantánea del estado de nuestra clase llamará al método GetState() y guardará el resultado. Veamos un ejemplo simple de implementación de un deshacer.

public class ClaseEjemplo
{
  private Struct MemItem
  {
    public Originador orig;
    public object estado;
  }

  Stack<MemItem> Memoria = new Stack<MemItem>();

  public void OnOperation(Originador target)
  {
    object state = target.GetState();
    Memoria.Push(new MemItem{orig = target, estado = state);
  }

  public void Deshacer()
  {
    MemItem itm = Memoria.Pop();
    itm.orig.SetState(itm.estado);
  }
}

Delphi

El RTI de Delphi no es tan bueno como el que nos proporciona .NET aunque nos sigue permitiendo el mismo nivel de flexibilidad, el código sería muy parecido.

interface

type Originador = class
  private
    name : string;
    x : integer;
    y : integer;
  public
    function GetState : TObject;
    procedure SetState(memento : TObject);
end;

implementation

type Memento = class
  public
    Name : string;
    X : integer;
    Y : integer;
end;

function Originador.GetState : TObject;
begin
  result := Memento.Create;
  result.Name := Self.name;
  result.X := Self.x;
  result.Y := Self.y;
end;

procedure Originador.SetState(memento : TObject);
var
  mem : Memento;
begin
  mem := memento as Memento;
  if Assigned(mem) then
  begin
    Self.name = mem.Name;
    Self.x = mem.X;
    Self.y = mem.Y;
  end;
end;

Ejemplos reales

En general, como ya he mencionado, el patrón memento se utiliza fundamentalmente en aplicaciones que necesiten guardar un estado o una serie de estados para volver a ellos cuando se desee.

Ejemplos reales podrían consistir en una aplicación de dibujo, en la que cada cambio que se realiza y se quiere proporcionar la posibilidad de deshacer, se almacena el estado de los objetos que se verán afectados por dicho cambio y se mete en una pila de cambios, de forma que, en un momento dado, para volver atrás bastará con recuperar paso a paso los estados anteriores.

Otro uso común del patron memento se da en el caso de los wizards o formularios de varios pasos en los que queremos ser capaces de permitir al usuario volver a pasos anteriores. Por ejemplo, en un formulario en el cual el usuario primero introduce sus datos personales, después sus datos fiscales y después otros datos relacionados, podemos utilizar el patrón para ir almacenando los distintos estados por los que pasa el usuario, pudiendo volver atrás a cualquier punto anterior de los pasos que ha ido dando el usuario.

  1. 1. RTI significa Run Time Information y representa la posibilidad de obtener información sobre clases y objetos en tiempo de ejecución del programa
4.4
Average: 4.4 (5 votes)
Your rating: None