Specification Pattern no Domain Driven Design

Vamos falar um pouco sobre o Specification Pattern no Domain Driven Design. Esse padrão é importante para reduzir o impacto de alterações na regra de negócio em um agregado em particular. Tais alterações além de poderem infringir o padrão Open/Closed do SOLID elas impactam nos testes desenvolvidos e modificam a complexidade de uma estrutura previamente estabilizada. Esse padrão pode ser utilizado em abordagens diferentes, mas é especialmente funcional com Domain Driven Design.

Aqui no blog temos uma série com vários artigos relacionados a Domain Driven Design e outros elementos relacionados ao design de software. Caso seja de interesse, fique a vontade para lê-los:

Reduzindo o impacto nos testes unitários

Retomando rapidamente, o princípio SOLID possui nele o O, Open/Closed Principle ou OCP. Esse princípio diz que as classes devem estar abertas para extensão mas fechadas para modificação. Em resumo o princípio diz que após feita uma alteração em uma classe para evidenciar uma intensão do sistema, novas alterações não devem mais ser feitas nela. Entretanto deve ser possível estender novas estruturas para tal. Isso realmente é muito desafiador.

Além disso não podemos menosprezar os testes unitários dos sistemas. Assim, uma classe “fechada”, estável e bem definida (do ponto de vista do SOLID) deve ser testada na sua unidade. Entretanto, mudanças que porventura sejam necessárias podem invalidar ou gerar a necessidade de novos testes em estruturas que eram estáveis. Isso é realmente ruim.

Com isso é compreensível que alguma estratégia deve ser utilizada para suportar o Open/Closed e reduzir o impacto nos testes. O Specification Pattern pode auxiliar tanto no Domain Driven Design quanto em qualquer outra abordagem a reduzir o impacto nos testes e nas classes.

Anti-corruption layer (ACL) e sub-domínios conformistas

Quando estamos falando do domain driven design estratégico nos deparamos com a possibilidade de domínios conformistas, ou seja, que precisam de se adequar a outro domínio sempre que alterações externas ocorrem. Então, vamos a um exemplo bastante óbvio: imagine um sistema bancário que precise de se adequar às leis, normas ou portarias geradas pelo estado. Nesse caso não cabe negociação: uma vez alterada a lei o sistema precisa se adaptar em conformidade.

Nesses casos é muito comum se utilizar uma estrutura que separe os domínios da estrutura externa com uma camada de anti-corrupção (anti corruption layer ou ACL). Essa abordagem faz com que os impactos das alterações nesses domínios afetem apenas um domínio em específico e que os seus efeitos sejam contidos.

Note que situações como essa são muito alinhadas com o pattern de especificações, uma vez que ele se molda para suportar tais mudanças repentinas com o menor ruído possível.

Specification Pattern no Domain Driven Design

Como já comentamos o Specification Pattern não é uma exclusividade do Domain Driven Design mas funciona muito bem com essa abordagem. Nessa parte do artigo vamos utilizar um cenário como referência, demonstrar formas de utilização, a relação com a injeção de dependência e com Factory.

Cenário de referência

Vamos considerar o cenário de uma situação hipotética em que um empresa de registro de patentes precisa solicitar os registros ao INPI, mas ao mesmo tempo ela precisa estar em conformidade com as normas instituídas pela autarquia.

De modo geral o padrão Specification é simples e possui uma classe para cada especificação. Essa classe deve herdar de uma classe base ou implementar uma interface comum, garantindo assim capacidade de uso desacoplado. Além disso, de modo geral a classe base ou interface deve ter um método com retorno booleano para verificação da regra.

Do lado do consumidor, a classe Patente possui um método para DepositarPatenteJuntoAoINPI (utilizando a nomenclatura particular desse domínio). Esse método deve fazer uso da especificação adequada.

Implementação

A implementação é bastante simples. A seguir você pode ver um exemplo com a interface ILeiDaPropriedadeIndustrialSpecification e duas classes que a implementam, com regras particulares para cada norma.

// Especificação de leis de Propriedade Industrial
public interface ILeiDaPropriedadeIndustrialSpecification
{
     bool EhPossivelRegistrar(Patente patente);
}
// Fica proibido o pedido de registros de patentes da classe 40
public class Norma1234De1998: ILeiDaPropriedadeIndustrialSpecification
{
    public bool EhPossivelRegistrar(Patente patente)
    {
        return !(patente.Classe == 40 && patente.Status == "Pedido");
    }
}
// Norma 1234 de 1998 para de vigorar
// Agora patentes da classe 40 cujo resumo inclua a palavra 'vacina' ficam proibidos
public class Norma4567De1999: ILeiDaPropriedadeIndustrialSpecification
{
    public bool EhPossivelRegistrar(Patente patente)
    {
        return !(patente.classe == 40 && patente.Status == "Pedido" && patente.resumo.Contains("vacina"));
    }
}

A partir desse ponto vamos alterar a classe Patente para suportar a especificação de modo a proibir pedido de registros de patentes a depender da lei que vigora.

public class Patente
{
    public int Classe {get; private set;}
    public string Status {get; private set;}
    public string Resumo {get; private set;}
    public ILeiDaPropriedadeIndustrialSpecification LeiVigente {get; private set;}

    public Patente(int classe, string status, string resumo, ILeiDaPropriedadeIndustrialSpecification leiVigente)
    {
       Validar(classe, status, resumo);
       this.Classe = classe;
       this.Status = status;
       this.Resumo = resumo;
       this.LeiVigente = leiVigente;
    }

    public void Validar(int classe, string status, string resumo)
    {
        // Detalhes da validação da classe
    }

    public void DepositarPatenteJuntoAoINPI()
    {
        if(LeiVigente.EhPossivelRegistrar(this))
        {
           // Prosseguir com o pedido de registro
        }
    }
   
}

Specification e Factory

Outro cenário bastante comum é a necessidade de haver duas especificações que estão ativas ao mesmo tempo, porém, ou uma está válida ou outra, a depender das condições do item em questão. Nesse caso é comum haver uma classe de Factory que retorna a especificação que se aplica ao caso.

Considere como referência o exemplo demonstrado da implementação acima. A classe a seguir é uma pequena fábrica que entrega uma implementação concreta da ILeiDaPropriedadeIndustrialSpecification a depender de características específicas da patente.

public class LeiDaPropriedadeIndustrialSpecificationFactory
{
   public ILeiDaPropriedadeIndustrialSpecification ObterLeiCorrente(Patente p)
   {
       if(p.DataDePrioridade > new Datetime(2020,16,03))
             return new NormaVacinal_Lei1234De2020();
       else
             return new NormaComum_Lei8899De1988();

   }
}

Specification Composition

Esse já um cenário um pouco mais complexo e específico. Considere, por exemplo, que uma lei não substitui uma outra, mas, ao contrário, ela é uma nova condição para o domínio. Pense no exemplo indicado anteriormente: poderiamos ter 10 leis, umas para proibir coisas, outras para permitir, algumas vigorando e outras anulando leis em vigor. Nesse caso seria necessário fazer uma composição dessas especificações.

Veja a seguir o uso do Specification junto ao padrão Composition de modo a dar a possibilidade de fazer And, AndNot, Or, OrNot ou Not. Esse exemplo foi extraido desse link.

 public interface ISpecification<T>
    {
        bool IsSatisfiedBy(T candidate);
        ISpecification<T> And(ISpecification<T> other);
        ISpecification<T> AndNot(ISpecification<T> other);
        ISpecification<T> Or(ISpecification<T> other);
        ISpecification<T> OrNot(ISpecification<T> other);
        ISpecification<T> Not();
    }

    public abstract class LinqSpecification<T> : CompositeSpecification<T>
    {
        public abstract Expression<Func<T, bool>> AsExpression();
        public override bool IsSatisfiedBy(T candidate) => AsExpression().Compile()(candidate);
    }

    public abstract class CompositeSpecification<T> : ISpecification<T>
    {
        public abstract bool IsSatisfiedBy(T candidate);
        public ISpecification<T> And(ISpecification<T> other) => new AndSpecification<T>(this, other);
        public ISpecification<T> AndNot(ISpecification<T> other) => new AndNotSpecification<T>(this, other);
        public ISpecification<T> Or(ISpecification<T> other) => new OrSpecification<T>(this, other);
        public ISpecification<T> OrNot(ISpecification<T> other) => new OrNotSpecification<T>(this, other);
        public ISpecification<T> Not() => new NotSpecification<T>(this);
    }

    public class AndSpecification<T> : CompositeSpecification<T>
    {
        ISpecification<T> left;
        ISpecification<T> right;

        public AndSpecification(ISpecification<T> left, ISpecification<T> right)
        {
            this.left = left;
            this.right = right;
        }

        public override bool IsSatisfiedBy(T candidate) => left.IsSatisfiedBy(candidate) && right.IsSatisfiedBy(candidate);
    }

    public class AndNotSpecification<T> : CompositeSpecification<T>
    {
        ISpecification<T> left;
        ISpecification<T> right;

        public AndNotSpecification(ISpecification<T> left, ISpecification<T> right)
        {
            this.left = left;
            this.right = right;
        }

        public override bool IsSatisfiedBy(T candidate) => left.IsSatisfiedBy(candidate) && !right.IsSatisfiedBy(candidate);
    }

    public class OrSpecification<T> : CompositeSpecification<T>
    {
        ISpecification<T> left;
        ISpecification<T> right;

        public OrSpecification(ISpecification<T> left, ISpecification<T> right)
        {
            this.left = left;
            this.right = right;
        }

        public override bool IsSatisfiedBy(T candidate) => left.IsSatisfiedBy(candidate) || right.IsSatisfiedBy(candidate);
    }
    public class OrNotSpecification<T> : CompositeSpecification<T>
    {
        ISpecification<T> left;
        ISpecification<T> right;

        public OrNotSpecification(ISpecification<T> left, ISpecification<T> right)
        {
            this.left = left;
            this.right = right;
        }

        public override bool IsSatisfiedBy(T candidate) => left.IsSatisfiedBy(candidate) || !right.IsSatisfiedBy(candidate);
    }

    public class NotSpecification<T> : CompositeSpecification<T>
    {
        ISpecification<T> other;
        public NotSpecification(ISpecification<T> other) => this.other = other;
        public override bool IsSatisfiedBy(T candidate) => !other.IsSatisfiedBy(candidate);
    }

Outras especificações

Os exemplos passados consideram que os retornos são sempre booleanos, mas isso não é necessariamento o único jeito: a criatividade é o limite. Não é incomum ver, por exemplo, cálculos de preço, geração de strings ou mesmo a construção de objetos complexos como teor da especificação.

Injeção de dependência

Uma técnica que faz todo o sentido quando estamos utilizando o Specification Pattern é a injeção de dependência. Através dela é muito prático indicar qual é a classe que corresponde à especificação vigente sem alterar o domínio em si. O exemplo a seguir exibe o injetor de dependências padrão do .NET Core (na linguagem C#), mas pode-se utlizar qualquer um em qualquer linguagem que interessar.

namespace Contatos.Application.DI;

public class Inicializer
{
    public static void Configure(IServiceCollection services, string connection)
    {
        services.AddScoped(typeof(IRepository<ILeiDeProtecaoDoConsumidor>), typeof(Lei1234de1998));       
    }
}

Conclusão de Specification Pattern no Domain Driven Design

Por fim, um destaque deve ser dado ao fato de que o padrão Open/Closed do SOLID é desafiador. Isto posto, o padrão Specification auxilia enormemente nesses casos. O domínio da aplicação deve ser sempre preservado, de modo que mudanças não o alterem e não impacte em testes. O artigo dá exemplos práticos de como pode-se utilizar esse padrão para leis, normas etc, especialmente aplicável em estruturas que precisam funcionar em conformidade. Definitivamente o padrão de especificações é muito flexível e uma boa prática.


Thiago Anselme
Thiago Anselme - Gerente de TI - Arquiteto de Soluções

Ele atua/atuou como Dev Full Stack C# .NET / Angular / Kubernetes e afins. Ele possui certificações Microsoft MCTS (6x), MCPD em Web, ITIL v3 e CKAD (Kubernetes) . Thiago é apaixonado por tecnologia, entusiasta de TI desde a infância bem como amante de aprendizado contínuo.

Deixe um comentário