TypeScript

24 mai, 2023

Implementando o padrão Specification no Node.js com TypeScript

Publicidade

O padrão Specification é um padrão de projeto de software que permite definir regras de negócio em um formato legível e reutilizável. Ele é particularmente útil em aplicações que precisam implementar regras de negócio complexas que envolvem múltiplas entidades. Neste artigo, vamos ver como implementar o padrão Specification em uma aplicação Node.jscom TypeScript.

Definindo a entidade

Para exemplificar o uso do padrão Specification, vamos criar uma entidade simples chamada Transaction. Ela possui três propriedades: amount (valor da transação), date (data da transação) e description (descrição da transação).

Crie um arquivo chamado transaction.ts na pasta src/entities com o seguinte conteúdo:

export class Transaction {
  constructor(
    public readonly amount: number,
    public readonly date: Date,
    public readonly description: string,
  ) {}
}

 

Esta classe representa uma transação financeira com os seguintes atributos:

amount: o valor da transação
date: a data da transação
description: uma descrição da transação

Criando as regras de negócio

Agora que temos a entidade de transação, vamos criar as regras de negócio para verificar se uma transação é válida ou não. Crie um arquivo chamado specification.ts na pasta src/specifications com o seguinte conteúdo:

export interface Specification<T> {
  isSatisfiedBy(candidate: T): boolean;
  and(other: Specification<T>): Specification<T>;
  or(other: Specification<T>): Specification<T>;
  not(): Specification<T>;
}

export class CompositeSpecification<T> implements Specification<T> {
  public isSatisfiedBy(candidate: T): boolean {
    throw new Error("Not implemented");
  }

  public and(other: Specification<T>): Specification<T> {
    return new AndSpecification(this, other);
  }

  public or(other: Specification<T>): Specification<T> {
    return new OrSpecification(this, other);
  }

  public not(): Specification<T> {
    return new NotSpecification(this);
  }
}

class AndSpecification<T> extends CompositeSpecification<T> {
  constructor(
    private readonly left: Specification<T>,
    private readonly right: Specification<T>
  ) {
    super();
  }

  public isSatisfiedBy(candidate: T): boolean {
    return (
      this.left.isSatisfiedBy(candidate) && this.right.isSatisfiedBy(candidate)
    );
  }
}

export class OrSpecification<T> extends CompositeSpecification<T> {
  constructor(
    private readonly left: Specification<T>,
    private readonly right: Specification<T>
  ) {
    super();
  }

  public isSatisfiedBy(candidate: T): boolean {
    return (
      this.left.isSatisfiedBy(candidate) || this.right.isSatisfiedBy(candidate)
    );
  }
}

export class NotSpecification<T> extends CompositeSpecification<T> {
  constructor(private readonly specification: Specification<T>) {
    super();
  }

  public isSatisfiedBy(candidate: T): boolean {
    return !this.specification.isSatisfiedBy(candidate);
  }
}
Este arquivo define uma interface Specification que representa uma regra de negócio e uma classe CompositeSpecification que implementa a lógica de combinação de regras de negócio.

As classes AndSpecification, OrSpecification e NotSpecification são subclasses de CompositeSpecification que implementam as operações lógicas AND, OR e NOT, respectivamente.

Crie um arquivo chamado transaction.ts na pasta src/specificationscom o seguinte conteúdo:

import { Transaction } from "../entities/transaction";
import { CompositeSpecification } from "./specification";

export class TransactionSpecification extends CompositeSpecification<Transaction> {
  public isSatisfiedBy(candidate: Transaction): boolean {
    return TransactionSpecification.amountIsGreaterThan(50)
      .and(TransactionSpecification.dateIsGreaterThan(new Date(2022, 0, 1)))
      .and(TransactionSpecification.descriptionContains("supermercado"))
      .isSatisfiedBy(candidate);
  }

  public static amountIsGreaterThan(value: number): TransactionSpecification {
    return new (class extends TransactionSpecification {
      public isSatisfiedBy(candidate: Transaction): boolean {
        return candidate.amount > value;
      }
    })();
  }

  public static amountIsLessThan(value: number): TransactionSpecification {
    return new (class extends TransactionSpecification {
      public isSatisfiedBy(candidate: Transaction): boolean {
        return candidate.amount < value;
      }
    })();
  }

  public static dateIsGreaterThan(value: Date): TransactionSpecification {
    return new (class extends TransactionSpecification {
      public isSatisfiedBy(candidate: Transaction): boolean {
        return candidate.date > value;
      }
    })();
  }

  public static dateIsLessThan(value: Date): TransactionSpecification {
    return new (class extends TransactionSpecification {
      public isSatisfiedBy(candidate: Transaction): boolean {
        return candidate.date < value;
      }
    })();
  }

  public static descriptionContains(value: string): TransactionSpecification {
    return new (class extends TransactionSpecification {
      public isSatisfiedBy(candidate: Transaction): boolean {
        return candidate.description.includes(value);
      }
    })();
  }
}
A classe TransactionSpecification é uma subclasse de CompositeSpecification que define as regras de negócio específicas para a entidade Transaction.

As funções estáticas amountIsGreaterThan, amountIsLessThan, dateIsGreaterThan, dateIsLessThan e descriptionContains retornam instâncias de TransactionSpecification que representam cada uma das regras de negócio. O método isSatisfiedBy contem a composição da regra. a classe esta com as subsclasses publica a modo que ao criar uma outra regra pode ser apenas ajustada e extendida para compor.

Utilizando as regras de negócio

Agora que temos as regras de negócio definidas, podemos utilizá-las para validar uma transação. Crie um arquivo chamado transaction.ts na pasta src/validators com o seguinte conteúdo:

import { Transaction } from './Transaction';
import { TransactionSpecification, Specification } from './TransactionSpecification';

export class TransactionValidator {
  constructor(private readonly specification: Specification<Transaction>) {}

  public validate(transaction: Transaction): boolean {
    return this.specification.isSatisfiedBy(transaction);
  }
}

// testando o validator
const transaction = new Transaction(100, new Date(), 'Compra no supermercado');

const validator = new TransactionValidator()

console.log(validator.validate(transaction)); // true


Este arquivo define a classe TransactionValidator, que recebe uma instância de Specification<Transaction> no construtor e possui um método validate que recebe uma transação e retorna um valor booleano indicando se a transação é válida ou não.

No exemplo acima, criamos uma transação com valor 100, data new Date() e descrição ‘Compra no supermercado’. Em seguida, criamos uma instância de TransactionValidator as regra de negócio verifica se o valor da transação é maior que 50, se a data da transação é maior que 01/01/2022 e se a descrição da transação contém a palavra ‘supermercado’. Finalmente, chamamos o método validate passando a transação criada e exibimos o resultado no console.

Conclusão

Neste artigo, vimos como implementar o padrão Specification em uma aplicação Node.js com TypeScript. Criamos uma entidade Transaction, definimos regras de negócio para validar uma transação utilizando a classe TransactionSpecification e criamos um validador de transações utilizando a classe TransactionValidator.

O padrão Specification pode ser aplicado em diversos contextos e é uma boa opção quando precisamos implementar regras de negócio complexas que envolvem múltiplas entidades. Com ele, podemos tornar o código mais modular, fácil de manter e extensível.

Segue exemplo no github: https://github.com/jhonesgoncalves/example-specification-ts

 

*O conteúdo deste artigo é de responsabilidade do(a) autor(a) e não reflete necessariamente a opinião do iMasters.