Factory no Domain Driven Design

Factory no Domain Driven Design (DDD)

O Domain Driven Design ou DDD é uma abordagem estratégica e tática sobre como lidar com o software e o domínio do negócio. Ele traz uma forma em que ambos são indissociáveis, assim a comunicação entre os especialistas e os desenvolvedores tem menos ruído. Para o autor que escreveu sobre Factory no Domain Driven Design, Eric Evans, domínio é a questão mais fundamental do software e que, diferentemente das questões tecnológicas (como banco de dados, por exemplo) ela tem que ser do modo em que o especialista indicou.

Portando o DDD tem uma grande preocupação estratégica que se consolida nessa na forma de comunicação e em como entender o que se fala no projeto. A abordagem cria o conceito de linguagem ubíqua (ou linguagem universal) como ponto central do entendimento do domínio (ou sub-domínios) do negócio. Porém há também um conjunto de estruturas táticas que se aproximam da execução do projeto.

Note que há Agregados, Entidades, Objetos de Valor, e outros. Esse artigo se especializa no padrão Factory, detalhando sua origem, por que e como é utilizado. Caso prefira, para entender melhor esses outros aspectos do DDD, veja também nossa série de artigos sobre o tema:

Padrões do Domain Driven Design

O Domain Driven Design, como indicado no prólogo, traz uma abordagem estratégica e outra tática. A abordagem tática disserta sobre estruturas que podem se transformar em código de programação independentemente de linguagem (mas melhor alinhado com linguagens orientadas a objeto).

O DDD sustenta a ideia de que domínios ricos devem ter entidades ricas, ou seja, não anêmicas. Portanto não deve haver no domínio classes apenas com GETs e SETs, por exemplo. Mas ao contrário, classes devem ter métodos que expressem claramente a intensão do domínio.

Ao pensar desse modo estruturas como Agregados, Entidades e Value Objects emergem quase que naturalmente. Mas há características específicas quando se necessita armazenar tais dados em infraestruturas externas (como banco de dados, APIs externas, etc), utilizando Repositórios. Além disso há estruturas para facilitar a instância de agregados: as Factories. Há também estruturas para lidar com eventos de domínio, os Domain Events. E por fim, há uma estrutura especial para lidar com características do domínio que não são de uma entidades ou agregado em específico, através de Domain Services.

Patterns Gang of Four (GoF)

Assim como os padrões do Domain Driven Design, há um conjunto de padrões muito bem estabelecidos chamados de padrões GoF. Quarto indivíduos notaram formas de desenvolvimento ligeramente semelhantes que carregam boas práticas. Eles catalogaram esses padrões e compilaram no livro Design Patterns: Elements of Reusable Object-Oriented Software.

Há no livro quase trinta padrões, mas certamente Eric Evans se baseou em alguns padrões criacionais para formular o padrão Factory do DDD: Builder, Factory Method e Abstract Factory. Em breve teremos uma série aqui no blog falando detalhamente sobre os design patterns. Por ora vamos entender em linhas gerais quais são esses padrões para ajudar a contextualizar o Factory do DDD.

Builder

Esse é um padrão bastante conhecido e utilizado. Considere que você possui um objeto complexo que tem um construtor com muitos parametros e que tem muitas atividades. Considere também que essas atividades poderiam ser executadas em diferentes ordens. Bom, o builder provavelmente pode ser utilizado nesse cenário para simplificar a criação do objeto.

Factory Method

Mais um padrão criacional muito conhecido. Considere que há a necessidade de criar uma instância de um objeto de uma família de objetos, sem que o cliente tenha que conhecer concretamente quem é. Imagine que você tem uma empresa de logística com vários tipos de transporte. Sua aplicação precisa trabalhar com um transporte, mas não precisa saber qual. Uma possível solução é utilizar uma classe que produz a classe concreta sem que a parte cliente da aplicação precise desse detalhe, gerando assim certo desacoplamento.

Abstract Factory

Esse é um padrão semelhante ao Factory mas para cenários mais sofisticados. Nesse caso deve-se considerar um cenário que há diferentes famílias de objetos. Suponha que uma corretora de criptomoedas, por exemplo, deseje implementar suporte para duas criptomoedas: Bitcoin e Ethereum. Além disso, eles querem que seus clientes possam usar duas formas de pagamento: cartão de crédito e transferência bancária. Nesse cenário pode-se utilizar o Abstract Factory.

Factory no Domain Driven Design

Quando falamos de Factory para o DDD deve-se levar em consideração as inspirações do Eric ao observar os padrões criacionais. Ao mesmo tempo, deve-se observar que o Domain Driven Design pede por formas para facilitar a instância de objetos. Sempre considere essa máxima: Instanciar agregados deve ser uma operação simples.

Note que o estado de uma classe deve estar sempre consistente. Por conta disso é comum no DDD haver um construtor padrão que recebe parametros para todos os atributos que tem e, após isso, validá-los. Ocorre que nem sempre os cenários são desse modo. Há situações em que uma mesma classe pode ter alguns conjuntos de parametos preenchidos ao invés de outros. Há casos em que seria mais interessante que houvesse a instância de uma classe filha, a depender dos parametros. E há casos em que os objetos possuem escopos complexos. Nesses casos cabe o padrão Factory do DDD.

Cenário 1: Fluxos de inicialização distintos

Nesse cenário uma mesma entidade deve possui mais do que um construtor, mas garantindo o estado consistente do agregado. Note também que vários construtores diferentes nem sempre explicitam por que são e como são. O uso desse padrão muitas vezes oferece nomes para a estrutura de construção, algo que facilita muito o consumo.

Veja o código a seguir escrito em C# para um Produto. Ele possui os atributos Nome, Preço e CodigoSKU. Mas ocorre que, por padrão, o produto possui o SKU definido, mas, um produto completamente novo pode ainda não ter. Por conta disso, há uma estrutura apenas para suportar isso.

Para tal é necessário criar um construtor protected vazio, para que se possa instanciar o Produto sem parametros. O método estático NovoProdutoSemSKU faz uso desse construtor e gera um objeto específico conforme esperado.

using Anselme.Compras.Domain.Common;

namespace Anselme.Compras.Domain.Aggregates
{
    public class Produto : BaseEntity, IAggregateRoot
    {
        public string Nome { get; private set; }
        public decimal Preco { get; private set; }
        public SKU CodigoSKU { get; private set; }

        public Produto(string nome, decimal preco, SKU codigoSKU)
        {
            Validar(nome, preco, dataDeRegistro);
            this.Nome = nome;
            this.Preco = preco;
            this.CodigoSKU = codigoSKU;
        }

        protected Produto()
        {

        }

        public static Produto NovoProdutoSemSKU(string nome, decimal preco)
        {
            Produto produtoSemSKU = new Produto();
            produtoSemSKU.Nome = nome;
            produtoSemSKU.Preco = preco;
            produtoSemSKU.DataDeRegistro = DateTime.Now;

            return produtoSemSKU;
        }



        public void Validar(string nome, decimal preco, DateTime dataDeRegistro)
        {
            var validator = new ProdutoValidator();
            var result = validator.Validate(this);

            DomainException.ThrowIfIsFalse(result.IsValid,$"Produto inválido: {result.ToString()}");
        }
    }
}
// Uso prático do exemplo acima
Produto produto_ja_registrado = new Produto("Chinelo", 40m, new SKU("AVA-COM-TR1-AZU-42"));

Produto novo_produto = Produto.NovoProdutoSemSKU("Bolsa", 200m);

Cenário 2: Hierarquia de tipos

Ainda usando o exemplo do produto do cenário 1, considere que há regras específicas para a definição do preço. No exemplo a seguir há uma interface IRegraDePreco e classes que implementam essa interface, com as diversas regras existentes.

public interface IRegraDePreco
{
    decimal ObterPrecoLiquido(decimal precoBruto);
}

public class RegraDePrecoPadrao : IRegraDePreco
{
    public decimal ObterPrecoLiquido(decimal precoBruto)
    {
        return precoBruto;
    }
}

public class RegraDePreco2019Brasil : IRegraDePreco
{
    public decimal ObterPrecoLiquido(decimal precoBruto)
    {
        return precoBruto + 0.2m;
    }
}

public class RegraDePreco2020Exterior : IRegraDePreco
{
    public decimal ObterPrecoLiquido(decimal precoBruto)
    {
        return precoBruto + 0.6m;
    }
}

Agora note que temos uma classe chamada RegraDePrecoFactory que entrega uma instância de classe para IRegraDePreco a depender dos parametros indicados.

public class RegraDePrecoFactory
{
    public static IRegraDePreco RegraDePreco(Pais pais, DateTime dataDeRegistroDoProduto, decimal valorBruto)
    {
        if (dataDeRegistroDoProduto.Year == 2019)
        {
            switch (pais)
            {
                case "Brasil": return new RegraDePreco2019Brasil();
                default: return new RegraDePreco2020Exterior();
            }
        }

        return new RegraDePrecoPadrao();

    }
}

Por fim, a classe produto utiliza a fabrica para a definição do preço, de modo que a classe Produto não precisa ser alterada a medida que novas regras de preço surgem.

using Anselme.Contatos.Domain.Common;

namespace Anselme.Contatos.Domain.Aggregates
{
    public class Produto : BaseEntity, IAggregateRoot
    {
        public string Nome { get; private set; }
        public decimal Preco { get; private set; }
        public SKU CodigoSKU { get; private set; }

        public Produto(string nome, decimal preco, SKU codigoSKU, Pais pais)
        {
            Validar(nome, preco, dataDeRegistro);
            this.Nome = nome;
            this.Preco = RegraDePrecoFactory.ObterRegraDePreco(pais, codigoSKU.dataDeRegistro, preco).ObterPrecoLiquido(preco);
            this.CodigoSKU = codigoSKU;
        }

        public void Validar(string nome, decimal preco, DateTime dataDeRegistro)
        {
            var validator = new ProdutoValidator();
            var result = validator.Validate(this);

            DomainException.ThrowIfIsFalse(result.IsValid, $"Produto inválido: {result.ToString()}");
        }
    }
}

Cenário 3: Objetos complexos

Nesse cenário o código específico desse agregado é grande e que talvez não valha a pena te-lo todo dentro do mesmo código. Assim aumenta-se a expressividade do código a medida que a regra fica separada, seguindo a estrutura do Pattern Builder.

public class FormaEntrega {
    public string Nome { get; set; }
    public decimal Preco { get; set; }
    public int PrazoEntregaDias { get; set; }
}

// Builder para construir a forma de entrega do produto
public class FormaEntregaBuilder {
    private FormaEntrega _formaEntrega = new FormaEntrega();

    public FormaEntregaBuilder ComNome(string nome) {
        _formaEntrega.Nome = nome;
        return this;
    }

    public FormaEntregaBuilder ComPreco(decimal preco) {
        _formaEntrega.Preco = preco;
        return this;
    }

    public FormaEntregaBuilder ComPrazoEntregaDias(int prazoEntregaDias) {
        _formaEntrega.PrazoEntregaDias = prazoEntregaDias;
        return this;
    }

    public FormaEntrega Build() {
        return _formaEntrega;
    }
}

Já o código a seguir é o Produto, semelhante aos exemplos anteriores, mas com o método DefinirFormaEntrega que faz uso de uma Factory ao consumir o builder de forma de entrega.

using Anselme.Contatos.Domain.Common;

namespace Anselme.Contatos.Domain.Aggregates
{
    public class Produto : BaseEntity, IAggregateRoot
    {
        public string Nome { get; private set; }
        public decimal Preco { get; private set; }
        public SKU CodigoSKU { get; private set; }
        public int PrazoEntregaDias { get; set; }

        public Produto(string nome, decimal preco, SKU codigoSKU)
        {
            Validar(nome, preco, dataDeRegistro);
            this.Nome = nome;
            this.Preco = preco;
            this.CodigoSKU = codigoSKU;
        }

        public void DefinirFormaEntrega(string nome, decimal preco, int prazoEntregaDias)
        {
            var formaEntrega = new FormaEntregaBuilder()
                .ComNome(nome)
                .ComPreco(preco)
                .ComPrazoEntregaDias(prazoEntregaDias)
                .Build();

            this.PrazoEntregaDias = formaEntrega.PrazoEntregaDias;
        }


        public void Validar(string nome, decimal preco, DateTime dataDeRegistro)
        {
            var validator = new ProdutoValidator();
            var result = validator.Validate(this);

            DomainException.ThrowIfIsFalse(result.IsValid, $"Produto inválido: {result.ToString()}");
        }
    }
}

Esse último trecho de código demonstra o uso do produto com a definição da forma de entrega, que internamente faz uso do builder construído.

// Cria um objeto de ProdutoFisico
Produto chinelo = new Produto("Chinelo", 40m, new SKU("AVA-COM-TR1-AZU-42"));

// Define a forma de entrega do produto com o Builder
produto.DefinirFormaEntrega("Entrega Expressa", 25.00m, 3);

Conclusão de Factory no Domain Driven Design

A Factory do Domain Driven Design (DDD) é um padrão tático que pode ser utilizado de modos diferentes, com o objetivo de facilitar a geração de novos agregados. Lembre-se que a instância de objetos tem que ser fácil, mas isso nem sempre é aplicado desse modo nos códigos que vemos no dia-a-dia.


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