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.js
com 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);
}
}
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/specifications
com 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);
}
})();
}
}
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.