En anteriores artículos del patrón command y patrón command con manager iniciamos el camino sobre este patrón con alguna mejora. Ahora para terminar esta serie de artículos sobre la implementación del patrón command, vamos a utilizar una cola (queue) de comandos para que se ejecuten uno tras otro. No eliminaremos la ejecución que hacíamos hasta ahora, pero mediante alguna lógica podremos decidir si un comando lo llevamos a la cola o no.

Cola

Funcionamiento

Cada vez que la cola reciba un comando lo ejecutará inmediatamente y cuando termine su ejecución (acabe bien o mal) cogerá el siguiente comando de la cola y así sucesivamente hasta que no tenga más elementos.

Caso de uso

Un motivo por el cual queremos implementar una cola es la necesidad de que dos comandos se ejecuten uno tras otro. 

En nuestro  caso vamos a suponer que el identificador de la factura nos lo provee el backend. Así pues no podemos enviar una modificación sin antes saber el identificador de la factura por lo que enviaremos la creación de la factura a la cola y a partir de aquí si  queremos modificar cualquier propiedad y todavía no tenemos el identificador también llevaremos ese comando a la cola.

En resumen, los comandos que son dependientes los llevaremos a la cola.

export class Queue {
 
    private _queue: Array<ICommand> = [];
    private _locked: boolean = false;
 
    public enqueue(command: ICommand): void {
        this._queue.push(command);
        if (!this._locked) {
          this.next();
        }
    }
 
    public clear(): void {
      this._queue = [];
    }
 
    private next(): void {
      this._locked = true;
      const finished = () => {
        this._queue.length > 0 ? this.next() : this._locked = false;
      };
      const command: ICommand | undefined = this._queue.shift();
      if (!command){
        finished();
      }
      else {
        command.execute().then(finished).catch(finished);
      }
 
    }
 
  }

Idea de cola obtenida de https://stackblitz.com/edit/promise-queue

Comandos

Los comandos que teníamos hasta ahora eran servicios de angular que se ejecutaban cada vez que eran llamados. Ahora cambiaremos el funcionamiento de esos servicios para que se conviertan en builders ICommandService y devuelvan un objeto de la clase ICommand. También llevará una copia del modelo que había en el momento para que tenga una foto del momento. 

export interface ICommandService {
   getCommandName: () => CommandName;
   build(): ICommand;
}
export interface ICommand {
   execute(): Promise<void>;
   enqueue(): boolean;
}

En la clase comando se implementa un nuevo método “enqueue” que servirá al invoker saber si llevar ese comando a la cola o no.

public invokeInvoiceCommand(commandName: CommandName): void {
    const commandService: ICommandService | undefined = this._commandManagerService.getCommand(commandName);
    const command: ICommand | undefined = commandService?.build();
    if (!this.isCommand(command)) {
      throw new Error('No command!');
    }
 
    if (command.enqueue()){
      console.log('queuing command: ' + commandName);
      this._queue.enqueue(command);
    }
    else {
        command.execute();
    }
 
  }

El servicio de angular que crea el comando y su comando:

@Injectable({
  providedIn: 'root'
})
export class ModifyTotalAmountCommandService implements ICommandService {
 
  public constructor(
    private _modelService: ModelService,
    private _validatorService: ValidatorService,
    private _modifyTotalAmountCommandBuilderService: ModifyTotalAmountCommandBuilderService,
    private _modifyTotalAmountCommandDataService: ModifyTotalAmountCommandDataService) {
  }
 
  public getCommandName = () => CommandName.ModifyTotalAmount;
 
  public build(): ModifyTotalAmountCommand {
    const command: ModifyTotalAmountCommand = new ModifyTotalAmountCommand(
      this._modelService,
      this._validatorService,
      this._modifyTotalAmountCommandBuilderService,
      this._modifyTotalAmountCommandDataService
    );
    return command;
  }
 
 
}
 
export class ModifyTotalAmountCommand {
 
  private _model: IInvoice | undefined;
 
  public constructor(
    private _modelService: ModelService,
    private _validatorService: ValidatorService,
    private _modifyTotalAmountCommandBuilderService: ModifyTotalAmountCommandBuilderService,
    private _modifyTotalAmountCommandDataService: ModifyTotalAmountCommandDataService) {
      this._model = _modelService.getModelCloned();
  }
 
  public enqueue(): boolean {
    return this._model?.id === undefined;
  }
 
  public async execute(): Promise<void> {
    // Get model
    const invoice: IInvoice | undefined = this._modelService.getModel();
    if (!invoice || !this._model) {
      return;
    }
    if (!this.hasChanges(this._model)) {
      return;
    }
    if (this.isNew()) {
      return;
    }
 
    // Set id
    if (!this._model.id){
      this._model.id = invoice.id;
    }
 
    // Validate model
    if (this._validatorService.validate(this._model).length === 0) {
      // Build command
      const command: IModifyTotalAmountCommand = this._modifyTotalAmountCommandBuilderService.build(this._model);
      // Send command
      try {
          const result: IResultCommand = await this._modifyTotalAmountCommandDataService.send(command);
      } catch (Error) {
          console.log('Error');
      }
    }
  }
 
  private hasChanges(invoice: IInvoice): boolean {
    if (invoice) {
      return this._modelService.hasChanges(invoice, 'totalAmount');
    }
    return false;
  }
 
  private isNew(): boolean {
    return this._modelService.isNew();
  }
}

En el momento de la creación del comando, el servicio de angular le asigna una copia del modelo.

A partir de ahí en este comando dentro de su método execute hace sus comprobaciones como si hay cambios, si la factura es nueva o no, etc.

También dentro del servicio de modelo de la factura se ha ampliado su funcionalidad para saber si la factura es nueva y también comprobar si hay cambios.

Variantes de implementación

  • La cola se puede mejorar sustancialmente, pudiendo crear más métodos como encolar al principio o para que se inicie, se pare o pause según nos convenga. Incluso obtener librerías externas para ese cometido.
  • Cuando se crea el comando, se guarda una foto del modelo de ese momento. También se podría crear un memento y asignarle al comando a qué memento corresponde.

Conclusiones

Utilizamos una cola para poder ejecutar los comandos uno tras otro debido a que no se tiene el identificador de la entidad tratada (en este caso una factura).

Los servicios de angular serán unos builders para crear un objeto command que contiene internamente una «foto» del estado del modelo en ese momento.

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