El patrón Command en Angular

El patrón Command (perteneciente a los patrones de comportamiento)  nos ayuda a implementar el comportamiento de la aplicación desarrollada con interfaz inductiva u orientada a tareas (sea windows o web) . Este tipo de interfaz se adapta muy bien en aplicaciones con una arquitectura Event Sourcing y CQRS.

Entorno a usar el patrón Command

El ejemplo que vamos a implementar con el patrón command se basará en una arquitectura con una interfaz inductiva o basada en tareas suponiendo un backend implementado con event sourcing y CQRS.

En nuestro caso solo veremos la aplicación a nivel de nuestro cliente Angular y cómo ha de enviar diferentes mensajes o  intenciones de usuario al servidor mediante un comando. Dicho comando sólo será un objeto con propiedades que se enviará a un endpoint de una API. Cada endpoint recibirá un tipo de comando concreto teniendo tantos como diferentes comandos tengamos.

Interfaz inductiva o basada en tareas

Hace tiempo tuve el privilegio de formar parte de la presentación de un meetup del grupo “Tech Minders” junto a otros compañeros para transmitir la experiencia vivida en la creación de una aplicación distribuida. En ese encuentro expliqué la parte del desarrollo haciendo hincapié en las interfaces inductivas o basadas en tareas.

Podemos ver una explicación de cómo son y se diseñan las interfaces inductivas en la web de Microsoft. También se pueden ver los diferentes paradigmas y arquitecturas que se pueden encontrar desde el conocido CRUD (leer y enviar un DTO) hasta esta forma de enviar comandos (leer DTO y enviar mensajes) en este artículo.

Event Sourcing y CQRS

En este apartado, supondremos que el backend implementa el patrón Event Sourcing para ir guardando cada evento provocado por un comando. Existirá una API que recibirá todos esos comandos teniendo un endpoint diferente para cada comando.

Separando la parte de escritura de lectura implementará el patrón CQRS (Command Query Responsability Segregation) teniendo una consistencia eventual de datos. Las consultas se realizarán a otra API para ello.

Caso práctico

En este caso específico, realizaremos la lógica sobre el envío de una intención de usuario para modificar el campo “total” de una factura.

Command

Para empezar vamos a pensar que cada orden o comando será un servicio de angular (que en definitiva es una clase) debiendo implementar una interfaz común.

export interface ICommandService {
 execute(): Promise<void>;
}

Este servicio deberá implementar el método “execute” que devolverá una Promise dado que vamos a hacer peticiones al servidor teniendo en cuenta las llamadas asíncronas.

@Injectable({
  providedIn: 'root'
})
export class ModifyTotalAmountCommandService implements ICommandService {
 
  public constructor(
    private _modelService: ModelService,
    private _validatorService: ValidatorService,
    private _modifyTotalAmountCommandBuilderService: ModifyTotalAmountCommandBuilderService,
    private _modifyTotalAmountCommandDataService: ModifyTotalAmountCommandDataService) {
     }
 
  public async execute(): Promise<void> {
    // Get model
    const invoice: IInvoice = this._modelService.getModel();
    // Validate model
    if (this._validatorService.validate(invoice).length === 0) {
      // Build command
      const command: IModifyTotalAmountCommand = this._modifyTotalAmountCommandBuilderService.build(invoice);
      // Send command
      try {
        const result: IResultCommand = await this._modifyTotalAmountCommandDataService.send(command);
      } catch (Error) {
        console.log('Error');
      }
    }
  }
}

Dentro del servicio se realizará toda la lógica que requiera, en este caso, se recoge el modelo almacenado en un servicio (ModelService) y se valida  (ValidatorService). 

Si el modelo es válido, se creará el comando a enviar mediante un servicio de creación del comando (commandBuilderService) que mapea solamente propiedades. Finalmente se envía la intención de usuario al servidor (DataService).

Invoker

Para crear el invoker, también crearemos una interfaz para que cumpla un contrato.

export interface IInvokerService
{
    setCommand(command: ICommandService): void;
    invoke(): Promise<void>;
} 

Con esta interfaz obligamos a que se pueda asignar un comando e invocarlo quedando de la siguiente manera:

@Injectable({
  providedIn: 'root'
})
export class InvoiceInvokerService implements IInvokerService {
 
  private _command?: ICommandService = undefined;
 
  public setCommand(command: ICommandService): void {
    this._command = command;
  }
 
  public async invoke(): Promise<void> {
    if (this.isCommand(this._command)) {
        return await this._command.execute();
    }
    else {
      throw new Error('No command set');
    }
  }
 
  private isCommand(object: ICommandService | undefined): object is ICommandService {
    return object?.execute !== undefined;
  }
}

Cada vez que queramos enviar un comando al servidor, deberemos asignar al invoker el comando y ejecutarlo.

Por ejemplo si deseamos enviar el comando de modificación del total cada vez que se salga del campo (input) podría hacerse de la siguiente manera:

  public async totalAmountBlur($event: any): Promise<void> {
    this._invoiceInvokerService.setCommand(this._modifyTotalAmountCommandService);
    await this._invoiceInvokerService.invoke();
  }

A partir de aquí, se abre un abanico de posibilidades para modificar el invoker. Por ejemplo en vez de ir asignando un comando y ejecutarlo cada vez , se podría configurar con todos los comandos posibles desde el princpio (clases) y luego ir llamando a cada uno con un invoke diferente. También se puede configurar el invoker para que pueda ir encolando comandos y que se vayan ejecutando a medida que termine cada uno.

Esta podría ser una visión para implementar en un entorno arquitectónico como se ha comentado al principio. Seguramente se puede adaptar para mejorar su usabilidad, así que si tienes alguna idea, te animo a que la expongas.

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

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *