En este artículo veremos el patrón Memento (perteneciente a la familia de patrones de comportamiento). Crearemos un ejemplo de aplicación de este patrón Memento en typescript para Angular.

Dado que el modelo que vamos a guardar puede ser bastante complejo necesitaremos de alguna librería externa o de terceros para la clonación de objetos. Ha de quedar claro que en typescript un simple Object.assign o un JSON.parse/JSON.stringify no siempre es suficiente para clonar un objeto complejo, por eso nos apoyaremos en una librería externa con sus pros y contras.

Por lo tanto utilizaremos el patrón Adapter  ó Wrapper (perteneciente a la familia de patrones estructurales) para encapsular la funcionalidad de clonación que nos ofrece esa librería. Este patrón Adapter nos permitirá en un futuro poder cambiar esta librería de terceros por otra sin que afecte al resto de nuestra lógica (o las llamadas que hagamos a sus métodos).

Librerías hay muchas, como por ejemplo:

  1. Lodash/cloneDeep
  2. Fast-Clone
  3. Clone

Cada una de ellas tiene su peculiaridades. En nuestro caso podemos usar Fast-Clone, si en cambio necesitamos alguna personalización para clonar  podríamos usar lodash.

Puesto que hablamos de clonación de objetos, crearemos una clase abstracta que tendrá un método común de clonación siguiendo el patrón Template method del que ya vimos también en el artículo sobre patrón Strategy. ¡Cuidado! No es usar el patrón Prototype, puesto que no vamos a clonar toda clase ni un prototipo, solo se pretende clonar el modelo que contenga una clase para construir el Memento.

Es recomendable al menos echar un vistazo a esos patrones para entender su uso. No son nada complicados de entender. 

 Volviendo al patrón original que nos atañe, ¿En qué casos podemos usar el patrón Memento? Básicamente nos puede servir para restaurar el estado de algún modelo en caso de necesidad.

Ejemplos:

  1. Poder verificar si han habido modificaciones con respecto a un modelo original. Si hay alguna modificación, se podría enviar una modificación al servidor.
  2. Al enviar una modificación al servidor, si devuelve un error, poder restaurar los valores anteriores guardados.
  3. Dentro de la propia aplicación hacer “Undos”.

Caso práctico

Se necesita un servicio de Angular que guarde el estado de un modelo de manera que se pueda guardar una «fotografía» del mismo y se pueda restaurar.

Patrón Memento

Para empezar, definiremos la interfaz y clase que tendrá el Memento, es decir, donde se almacenará el estado del modelo/entidad.

export interface IMemento<T> {
  getMemento(): T;
}
 
export class Memento<T> implements IMemento<T> {
  private _model: T = null;
 
  public constructor(data: T) {
    this._model = data;
  }
 
  public getMemento(): T {
    return this._model;
  }
}

Al constructor del memento se le pasará el objeto a guardar y se crea además un método en la clase para recuperar dicho objeto, todo con la ayuda de los genéricos.

Antes de seguir con el uso del memento, ha de quedar claro que no deseamos guardar una referencia a un modelo, queremos guardar una copia (clon) de ese objeto, con lo que necesitaremos alguna herramienta para generar clones que veremos a continuación.

Patrón Adapter

Para obtener un clon en typescript de un objeto complejo no nos vale un simple Object.assign o un JSON.parse/JSON.stringify dadas sus carencias, necesitamos ayuda de alguna herramienta o librería externa que nos ayude a realizar una clonación profunda (o deep-cloning).

Existen muchas librerías que realizan ese trabajo y cada una tiene su forma de ser llamada. Para aislar la forma de utilizarse crearemos un wrapper para envolver su trabajo y de cara al exterior siempre se llamará al mismo método que definiremos. De esta manera si en algún momento cambiamos de librería solo se necesitaría modificar el wrapper, pero no el resto de clases que llamen al wrapper.

En nuestro caso instalamos fast-clone:

Instalación:

npm install fast-clone --save

Configuración de typescript (tsconfig.json) dado que esta librería trabaja en módulos UMD (no ES6):

"allowSyntheticDefaultImports": true

El primer paso será definir la interfaz de este Adapter sobre la librería de clonar:

export interface ICloneAdapter<T> {
  clone(data: T): T;
}

Luego crearemos la clase Adapter o Wrapper que llamará a la librería de clonación:

export class CloneAdapter<T> implements ICloneAdapter<T> {
  public clone(data: T): T {
    //Call the third-party library-method
    return clone(data);
  }
}

En este caso el método de clonación de la librería de terceros también tiene un método igual al del adapter, pero bien podría ser diferente dado que en el import de la clase ya se le da ese nombre:

import clone from "fast-clone";

Ahora solo hace falta que quien quiera clonar solo importe la clase “CloneAdapter” y llame al método “clone”.

Patrón Template Method

Por definición, este patrón nos ayuda a crear un mismo comportamiento. En nuestro caso, crearemos una clase abstracta que implementa un método de clonación y que heredarán las demás subclases.

Esta clase será la que llame a la librería de clonación, o más bien importará la clase adapter o wrapper de clonación y hará uso de ella:

import { CloneAdapter } from "../adapter/cloneAdapter";
 
export abstract class ClonableTemplate<T> {
  protected clone(data: T): T {
    const cloneAdapter: CloneAdapter<T> = new CloneAdapter();
    return cloneAdapter.clone(data);
  }
}

Podemos crear ahora otra clase abstracta que herede del ClonableTemplate que contenga toda la lógica o comportamiento que queremos que tengan aquellos servicios de angular que guarden un modelo.

Esta lógica tendrá la creación y recuperación del memento (entre otras) y hará uso de la clonación para crear copias de un modelo para esa creación del memento.

import { ClonableTemplate } from "./clonableTemplate";
import { Memento } from "../memento/memento";
 
export interface IModelService<T> {
  initialize(): void;
  makeModelBackup(): void;
  restoreModelBackup(): void;
  getModel(): T;
  setModel(value: T): void;
  toString(): string;
}
 
export abstract class ModelTemplateService<T> extends ClonableTemplate<T>
  implements IModelService<T> {
  private _memento: Memento<T> = null;
  private _model: T = null;
 
  public initialize(): void {
    this._model = null;
    this._memento = null;
  }
 
  public makeModelBackup(): void {
    this._memento = new Memento(this.clone(this._model));
  }
 
  public restoreModelBackup(): void {
    this._model = this._memento.getMemento();
  }
 
  public getModel(): T {
    return this._model;
  }
 
  public setModel(value: T): void {
    this._model = value;
  }
 
  public toString(): string {
    if (this._model) {
      return JSON.stringify(this._model);
    }
    return "";
  }
}

El método de la clase “makeModelBackup()”, clona el modelo almacenado en la clase para dárselo al constructor del memento. El memento se asigna a una variable interna para su almacenamiento.

Para recuperar el estado anterior del modelo, basta con llamar al método “restoreModelBackup()” que le asigna al modelo guardado el contenido del memento.

Servicio Angular

Ahora podemos crear un servicio de Angular que herede del template. El servicio solo actúa para guardar un modelo en concreto y poder manipularlo. Se crearían tantos servicios como modelos tengamos. Por ejemplo tenemos un modelo Persona:

export interface ICountry {
  id?: number;
  name?: string;
  code?: string;
}
 
export interface IPerson {
  id?: string;
  name?: string;
  country?: ICountry;
  birthDate?: Date;
}

Y un servicio de Angular que almacenará dicho tipo de modelo:

import { Injectable } from "@angular/core";
import { ModelTemplateService } from "../patterns/templateMethod/modelTemplate.service";
 
import { IPerson } from "../entities/iPerson";
 
@Injectable()
export class PersonModelService extends ModelTemplateService<IPerson> {}

A partir de aquí ya es simplemente usar el servicio en cualquier componente, servicio …

Uso

export class AppComponent implements OnInit {
  public constructor(private _personModelService: PersonModelService) {}

  title = "Memento pattern";

  originalModel: string;
  modifiedModel: string;
  restoredModel: string;

  public ngOnInit(): void {
    //Create new person
    const myModel: IPerson = {
      id: "1",
      name: "test",
      country: {
        id: 1,
        name: "spain",
        code: "ES",
      },
      birthDate: new Date(),
    };

    //Store the person in model service
    this._personModelService.setModel(myModel);
    this.originalModel = this._personModelService.toString();

    //Make backup/memento
    this._personModelService.makeModelBackup();

    //Change values
    myModel.id = "2";
    myModel.name = "modified";
    myModel.country = null;
    myModel.birthDate.setDate(myModel.birthDate.getDate() - 1);
    this.modifiedModel = this._personModelService.toString();

    //Restore memento
    this._personModelService.restoreModelBackup();
    this.restoredModel = this._personModelService.toString();
  }
}

Ampliaciones

Control de cambios

Una ventaja de conservar un estado es la posibilidad de controlar si han habido cambios entre el modelo y el memento. Se podría crear un método “hasChangesModelWithBackup” dentro de la clase “ModelTemplateService” para que sea accesible para todas las clases bases a la vez que se pudiera sobre-escribir si fuera el caso.

Mejoras en el Template para varios Mementos

A partir de aquí, también se puede crear una “pila” (stack) de mementos para ir haciendo “fotografías” del modelo en ciertos momentos para que el usuario pueda ir deshaciendo (undo). O en vez de una pila otro tipo de estructura de datos para obtener el estado en cualquier momento del tiempo.

Eso sí siempre habrá que llevar tanto la gestión de la pila (push, pop) como de otro tipo de estructura (getMementoAt, removeMementoAt, removeAll, hasChangesWithMemenoAt…).

Conclusiones

Hemos aprendido a utilizar tres patrones a partir del uso del patrón Memento: El patrón template method que prácticamente se aplica al utilizar la herencia de clases y el patrón Adapter con el que podemos cambiar una librería “fast-clone” por “lodash” modificando solamente la clase Adapter ó Wrapper si fuera necesario.

Espero que este caso sirva para aprender más patrones y pueda ser útil utilizarlos en los diferentes desarrollos. Qué te ha parecido?

Puedes descargar el código del artículo en GitHub