[Pt. 2] Melhorando sua lógica com Specification Pattern
Introdução
Estamos desenvolvendo uma aplicação para verificar os sintomas da COVID-19 dos pacientes, e designá-los para a área correta através do Specification Pattern
Este post contem duas partes:
- [Pt. 1] Melhorando sua lógica com Specification Pattern
- [Pt. 2] Melhorando sua lógica com Specification Pattern
As regras básicas de negócio ja foram atendidas, e ao final do primeira parte, ficamos com a seguinte estrutura de pastas e arquivos.
# Estrutura inicial de pastas
├── src
│ ├── patient
| │ ├── patient.entity.ts
| │ ├── symptoms.enum.ts
| │ ├── patient.specification.ts
│ ├── specification
| │ ├── specification.class.ts
| │ ├── specification.interface.ts
├── package.json
├── pnpm-lock.yaml
└── .gitignore
Continuando...
Para continuarmos, precisar compor as regras existentes para criar as regras mais complexas. Que são estas:
-
O paciente precisa de UTI
- O Paciente deve possuir todos os sintomas comuns
- O Paciente deve possuir pelo menos um sintoma crítico
-
O paciente precisa de avaliação médica
- O Paciente deve possuir todos os sintomas comuns
- O Paciente deve possuir todos os sintomas menos comuns
- O Paciente deve possuir pelo menos um sintoma crítico
Para que isso seja possível, precisamos aprimorar a classe de Specification
e a interface de ISpecification
para que aceite o método and
e or
.
Adicione o método abstrato para a especificação and
e or
, que receba uma propriedade ISpecification
/* src/specification/specification.interface.ts */
export abstract class ISpecification<T> {
abstract isSatisfiedBy(target: T): boolean;
abstract and(other: ISpecification<T>): ISpecification<T>;
abstract or(other: ISpecification<T>): ISpecification<T>;
}
Adicione e implemente o método recém criado, ficará assim:
/* src/specification/specification.class.ts */
export abstract class Specification<T> implements ISpecification<T> {
abstract isSatisfiedBy(target: T): boolean;
and(other: ISpecification<T>): ISpecification<T> {
return new AndSpecification<T>(this, other);
}
or(other: ISpecification<T>): ISpecification<T> {
return new OrSpecification<T>(this, other);
}
}
Vamos agora criar as classes que implementam AndSpecification
e OrSpecification
, para disponibiliza-los globalmente para as classes que estendem Specification
.
Você pode usar como base a Tabela da Verdade (Table of Truth) para criar outras lógicas de comparação.
Implementação das classes
AndSpecification
/* src/specification/specification.class.ts */
export class AndSpecification<T> extends Specification<T> {
constructor(private readonly one: ISpecification<T>, private readonly other: ISpecification<T>) {
super();
}
isSatisfiedBy(target: T): boolean {
return this.one.isSatisfiedBy(target) && this.other.isSatisfiedBy(target);
}
}
export class AndSpecification<T> extends Specification<T>
Indicação das especificações para comparação no método isSatisfiedBy
, iniciando a classe principal através do método super()
constructor(
private readonly one: ISpecification<T>,
private readonly other: ISpecification<T>
) {
super();
}
Método que implementa a lógica de comparação para satisfazer a especificação "and" (&&)
isSatisfiedBy(target: T): boolean {
return this.one.isSatisfiedBy(target) && this.other.isSatisfiedBy(target);
}
OrSpecification
/* src/specification/specification.class.ts */
export class OrSpecification<T> extends Specification<T> {
constructor(private readonly one: ISpecification<T>, private readonly other: ISpecification<T>) {
super();
}
isSatisfiedBy(target: T): boolean {
return this.one.isSatisfiedBy(target) || this.other.isSatisfiedBy(target);
}
}
export class OrSpecification<T> extends Specification<T>
Indicação das especificações para comparação no método isSatisfiedBy
, iniciando a classe principal através do método super()
constructor(
private readonly one: ISpecification<T>,
private readonly other: ISpecification<T>
) {
super();
}
Método que implementa a lógica de comparação para satisfazer a especificação "or" ( || )
isSatisfiedBy(target: T): boolean {
return this.one.isSatisfiedBy(target) || this.other.isSatisfiedBy(target);
}
Regras de negócio compostas
Com as especificações de lógicas criadas, podemos criar as duas lógicas restantes.
- Paciente precisa de UTI
- Paciente precisa de avaliação médica
Paciente precisa de UTI
Para que essa regra de atendida, precisamos que outras regras sejam atendidas. Que são:
- O paciente deve possuir todos os sintomas comuns
- Regra criado em: HasAllCommonSymptomsSpecification
- O Paciente deve possuir pelo menos um sintoma crítico
- Regra criado em: HasSomeSeriousSymptomsSpecification
Agora, vamos criar uma classe chamada NeedsUTI
, e aplicar a lógica AND
. Seu código ficará dessa forma.
/* src/patient/patient.specification.ts */
export class NeedsUTI extends Specification<Patient> {
isSatisfiedBy(patient: Patient): boolean {
return new HasAllCommonSymptomsSpecification()
.and(new HasSomeSeriousSymptomsSpecification())
.isSatisfiedBy(patient);
}
}
Note que estamos seguinte do as regras de negócio estabelecidas na aplicação.
O Paciente deve conter todos os sintomas comuns e pelo menos um sintoma crítico para ir para a UTI.
Paciente precisa de atendimento médico
Para que essa regra de atendida, precisamos que outras regras sejam atendidas. Que são:
- O paciente deve possuir todos os sintomas comuns;
- Regra criado em: HasAllCommonSymptomsSpecification
- O Paciente deve possuir pelo menos um sintoma crítico;
- Regra criado em: HasSomeSeriousSymptomsSpecification
- O Paciente deve possuir pelo menos um sintoma menos comum;
- Regra criado em: HasSomeLessSymptomsSpecification
Agora, vamos criar uma classe chamada NeedMedical
, e aplicar a lógica OR
. Seu código ficará dessa forma.
/* src/patient/patient.specification.ts */
export class NeedMedical extends Specification<Patient> {
isSatisfiedBy(patient: Patient): boolean {
return new HasAllCommonSymptomsSpecification()
.or(new HasSomeSeriousSymptomsSpecification())
.or(new HasSomeLessSymptomsSpecification())
.isSatisfiedBy(patient);
}
}
O Paciente deve conter todos os sintomas comuns ou pelo menos um sintoma crítico ou pelo menos um sintoma menos comum para ir para o atendimento médico.
const patient = new Patient({
name: "Jhon Doe",
symptoms: ["PERDA_DE_FALA"],
});
const patientNeedMedical = new NeedMedical().isSatisfiedBy(patient);
if (patientNeedMedical) {
// Sua lógica
}
Agora a com a base e a lógica entendidas, podemos criar qualquer lógica para atender uma regra de negócio. Por exemplo:
O Paciente deve possuir sintomas comuns e um sintoma menos comum e não possuir nenhum sintomas críticos
/* src/patient/patient.specification.ts */
export class LessUrgent extends Specification<Patient> {
isSatisfiedBy(patient: Patient): boolean {
return new HasAllCommonSymptomsSpecification()
.and(new HasSomeLessSymptomsSpecification())
.not(new HasSomeSeriousSymptomsSpecification()) // Lógica não implementada
.isSatisfiedBy(patient);
}
}
Implemente a lógica NOT e faça as combinações que desejar e precisar.
Conclusão
Sempre que possível a utilização de um pattern para resolver um problema de lógica que se repete, ou que ficou muito complexa, é sempre bem vinda. O Specification Pattern
é sempre bom quando se tem regras de negócios que são compostas por outras regras de negócio. Mas lembre-se, podemos sempre cair na otimização prematura
da aplicação. Por isso, tenha cuidado ao utilizar patterns.