loading...
Cover image for Implementando Domain-Driven Design

Implementando Domain-Driven Design

guisfits profile image Guilherme Camargo ・10 min read

Após vermos os conceitos do DDD com os Strategic Patterns, está na hora de ver algum código. Neste artigo abordarei os Tactical Patterns.

Tactical Patterns

O objetivo dos Tactical Patterns é gerenciar a complexidade e garantir a expressividade do Domain Model.

Eric Evans agrupou alguns Design Patterns (Building Blocks) em seu livro, que são um conjunto de boas práticas em torno dos princípios de OOP. Todos esses patterns já existiam antes de Evans os agrupar e não são exclusivos do DDD, porém Evans conseguiu consolidá-los de tal maneira que se tornaram uma ferramenta poderosa quando queremos criar um design rico e expressivo em domínios complexos.

Entity

Uma Entity é um objeto que mantém um ciclo de vida independente do seu estado atual. Seus atributos podem mudar, porém podemos sempre identificá-las por algum campo específico, como um número inteiro ou um GUID, por exemplo.

Vamos pegar de exemplo uma cesta de compras de um e-commerce. Após criarmos uma Entity Basket ela naturalmente passará por mutação, o valor total, quantidade de itens e outros campos podem receber outros valores, porém sempre saberemos diferenciá-la pelo seu Id.

Em uma modelagem Table Oriented, onde cria-se classes com objetivo de representar tabelas em um banco relacional, deixa-se de considerar sua representatividade para o domínio, onde essas classes anêmicas são simplesmente sacolas de dados.

Você deve ser capaz de criar todo o seu domínio e testá-lo sem sequer saber qual banco de dados irá usar, seja relacional ou NoSQL.

A Entity é uma das peças fundamentais no nosso Domain Model e devemos criá-la exclusivamente para satisfazê-lo. Ela é composta não apenas por atributos primitivos (int, float, string...), mas também por outras Entities e objetos complexos - Value Objects.

É importante mantermos sua consistência. Em linguagens orientadas a objeto devemos tornar seus atributos privados de modo a blindá-los de modificações externas. Isso vai garantir que toda a mudança de estado passe por um método que aplicará possíveis regras.

A própria Entity deve saber se construir. É uma má-pratica não ter um construtor neste caso, pois se deixamos classes externas criar objetos sem considerar suas regras, eventualmente iremos ter objetos inconsistentes.

Quando temos comportamento correspondente a uma Entity, quem melhor para tratá-los do que ela mesma? É comum em modelagens anêmicas deixar todo o comportamento para classes externas, porém acabamos somente quebrando a classe em N pedaços e dividindo sua responsabilidade. Se algo do mundo real tem estado e comportamento, uma Entity deve representá-lo fielmente.

entity

Value Object

Diferentemente da Entity, um Value Object é definido por seus atributos.

Tome o clássico exemplo da classe Money. O que difere duas instâncias com o mesmo valor? Nada! Eles não mantêm um ciclo de vida, sendo criados, utilizados e descartados! Também são plenamente substituíveis por outro objeto do mesmo tipo e valor.

Value objects são imutáveis por definição. Assim como no mundo real você não consegue transformar R$ 5,00 em R$ 50,00, Value Objects também não permitem esta mudança. Para conseguir um novo valor devemos criar uma nova instância de seu tipo, nunca alterar seus atributos internos.

Uma Entity pode ser composta por N Value Objects. Como Value Objects não mantêm estado, não temos como identificá-los no banco de dados, portanto devemos persisti-lo junto a Entity, nunca separado.

Em bancos de dados orientado a documentos podemos salvá-lo dentro do mesmo objeto. Em bancos de dados relacionais podemos usar um ORM para mapear esses objetos para colunas específicas.

value-object

Domain-Service

Quando temos um comportamento não atrelado a uma Entity ou Value Object, podemos representá-lo como um Domain Service.

Domain Service tem as seguintes características:

  • Não mantêm estado
  • Não tem rastreabilidade
  • Podem receber Entities e Value Objects como parâmetro
  • Podem ter dependências de interfaces para serviços externos
  • Executam alguma regra de negócio.

Tome como exemplo o cálculo de taxa de envio de um pedido. Ele deve considerar os itens do carrinho, o peso de cada item e a distância do endereço de entrega. Logo, percebemos que toda essa lógica não se encaixa em nenhuma Entity ou Value Object. Um Domain Service é perfeito neste caso.

É importante mantermos a Ubiquitous Language sempre em mente e aplicá-la aqui também, tanto no nome da classe e funções, quanto internamente - em nome de variáveis, por exemplo. Você pode pedir para uma pessoa de negócio explicar toda a regra e traduzir esse entendimento para o código. Assim se algo estiver faltando você facilmente conseguirá descrever o algoritmo para o domain expert e receber um feedback de possíveis inconsistências.

domain-service

Aggregate

Para expressar uma ideia no código é comum ter uma árvore de classes que se relacionem e possuem dependências uma da outra. Porém, conforme o Domain Model se expande esta árvore acaba se tornando muito extensa e com várias ramificações. Isso dificultará seu carregamento do banco de dados, suas invariâncias ao fazer alterações e problemas de concorrência.

Aggregate-1

Com Aggregates conseguimos diminuir a complexidade de objetos grandes para pequenos grupos de classes que devem ter operações atômicas.

Uma operação atômica é uma ação que ocorre completamente independente de outro processo. Uma transaction num banco de dados relacional segue esse princípio. Assim que você inicia uma transaction, nenhum outro processo poderá ler ou gravar um novo valor até que o processo anterior seja finalizado, desta forma se garante a consistência da operação e dos dados.

Um Aggregate se comporta como uma única unidade em memória, tendo sempre uma Entity principal que será o Aggregate Root e a partir dela teremos acesso a outras Entities e Value Objects. Aggregates podem se relacionar mantendo o Id de outro Aggregate em seus atributos, mas nunca uma referência para outro objeto Aggregate.

Essa divisão deve seguir os conceitos do domínio e não apenas agrupar dados. Pense em suas invariâncias. Um Address só pode ser alterado se soubermos a quem ele pertence, então não faz sentido Address ser um objeto isolado já que ele está fortemente acoplado com Customer. Por sua vez, Customer é uma Entity forte, dado que conseguimos outras informações através dela. Neste cenário faz sentido termos um Aggregate chamado Customers, sendo a Entity Customer seu Aggregate Root.

Aggregate-2

Em bancos orientado a documentos temos a facilidade de poder persistir um Aggregate por documento e obtê-lo de uma única vez.

FakeCommerce.Store.Shopping.Domain
├── Baskets
│   ├── Basket.cs
│   ├── BasketItem.cs
│   └── Coupon.cs
├── Customers
│   ├── Address.cs
│   ├── CreditCart.cs
│   └── Customer.cs
└── Products
    ├── Price.cs
    ├── Product.cs
    └── Size.cs

Factory

Se um Aggregate é suficientemente complexo para ser construído podemos usar o pattern Factory. Ele é um dos patterns originais do GOF. Seu objetivo é criar objetos respeitando suas invariâncias, ou seja, ele só criará objetos que respeitem certas regras e estejam em estado válido.

Por padrão, devemos usar o método construtor presente em muitas linguagens. Uma Factory é útil somente em ocasiões em que o construtor fica sobrecarregado com regras, ou quando temos mais de uma maneira de construir o objeto.

factory

Repository

Agora que temos os objetos organizados por Aggregates precisamos de um meio para persistí-los e hidratá-los. O pattern Repository é um dos mais conhecidos e seu objetivo é abstrair o acesso a dados da camada de domínio. Ele se comporta como uma coleção de dados em memória. Para quem está consumindo é transparente, além de não criar acoplamento com o meio de persistência.

Devemos ter um Repository por Aggregate. Como o Aggregate se comporta como uma unidade, não faz sentido ter um Repository para cada classe dele, faz mais sentido persistir e hidratar esses objetos juntos.

O uso de Repositories acaba sendo extrapolado em alguns casos. Ele é bastante útil quando precisamos hidratar os dados na camada de domínio e para reaproveitar operações comuns, porém seu uso não é obrigatório. Se você precisa extrair um relatório do sistema ou obter dados, formatar e enviar através de uma API, por exemplo, um Repository acaba não ajudando muito, pois não se torna tão flexível. É importante mantermos o senso crítico e somente usá-lo quando trazer algum benefício.

Para usá-lo adequadamente basta criar uma interface na pasta do Aggregate e definir os métodos. Lembre-se de usar a Ubiquitous Language e evitar o nome do pattern na camada de domínio. Eu geralmente crio as interfaces com o nome do Aggregate no plural, ICustomers, por exemplo, assim indico o seu comportamento (uma coleção em memória) sem sujar a linguagem.

repository

Agora, com Dependency Inversion, podemos receber esta interface pelo construtor sem conhecer sua implementação que está na camada de infraestrutura.

Domain-Event

Domain Events são muito úteis quando queremos propagar ações ocorridas dentro do Domain Model.

Esse pattern se dá na criação de uma classe POCO com os dados que queremos propagar. Todo o Domain Event lançado já aconteceu, refletindo o passado. Events são imutáveis, já que uma vez que este evento ocorreu não tem como voltar e alterá-lo. Eles seguem o princípio Fire and Forget, já que uma vez lançado não temos mais controle de quem irá receber, podendo ser uma ou mais classes, devido a isso também eles não possuem nenhum retorno.

Temos algumas maneiras de implementá-lo:

  • Se o Domain Event será somente de uso interno - dentro do mesmo Bounded Context, podemos usar uma biblioteca como MediatR. Ela já possui uma interface chamada INotification que será aplicada no Domain Event e outra INoficationHandler que será aplicada em quem esta ouvindo este evento. Assim toda vez que você disparar um novo Domain Event todos os Handlers irão ser chamados imediatamente, pois tudo isso acontece em memória dentro do mesmo processo.
  • Se o Domain Event se propaga para outros Bounded Contexts podemos usar uma solução mais robusta, como o RabbitMQ, por exemplo. Com ele não precisamos criamos acoplamento entre os Bounded Contexts, e seus Handlers podem ficar em processos separados, aumentando a escalabilidade da aplicação.

domain event
domain event handler

Modules

E por último, mas não menos importante, vamos falar de Modules.

Para criar Modules em C# temos os namespace, que nada mais são do que um caminho virtual para as classes. Temos também os arquivos de extensão .csproj, esses servem para separar os arquivos fisicamente e gerenciar suas dependências. Outras linguagens tem conceitos parecidos - como packages em Java - mas só o caminho real já pode ser o suficiente na maioria dos casos.

Com o uso de Modules conseguimos manter a organização do projeto de maneira consistente e respeitar a estrutura da Ubiquitous Language. Esta estrutura deve nos dizer sua intenção e falar a linguagem do negócio ao mesmo tempo. Essa organização, entre outras coisas, ajuda:

  • A entender assuntos relacionados e manter assuntos distintos fisicamente separados.
  • Na manutenção, pois quando você precisar mexer em uma área saberá exatamente onde procurar.
  • A novos desenvolvedores entender o projeto e saber onde devem ou não mexer.
  • A evitar conflitos de classes com o mesmo nome.

Modules são usados para decompor o Domain Model. Um domínio grande terá múltiplos Bounded Contexts e esses terão múltiplos conceitos. Podemos considerar esses conceitos como os Modules. Por exemplo, quando o domain expert da Store fala sobre Shopping, espera-se que todo o código relacionado a este assunto esteja em um namespace como {CompanyName}.Store.Shopping. Modules se mantém entre o Bounded Context e os Aggregates.

Não há uma regra dizendo como você deve arquitetar seu projeto, isso é muito subjetivo e tem N fatores que influenciam, mas de modo a ilustrar um exemplo seguindo a divisão em camadas proposta por alguns autores, podemos ter algo parecido com isso:

└── FakeCommerce.Store
    └── FakeCommerce.Store.Shopping
        ├── FakeCommerce.Store.Shopping.Application
        ├── FakeCommerce.Store.Shopping.Domain
        ├── FakeCommerce.Store.Shopping.Integration
        └── FakeCommerce.Store.Shopping.Persistence
  • Application onde ficará os casos de uso e regras da aplicação.
  • Domain terá os Aggregates e as regras de negócio.
  • Integration será responsável pela comunicação com outros Bounded Contexts (Anti-Corruption Layer) e serviços externos.
  • Persistence cuidará da infraestrutura do banco de dados e terá a implementação dos Repositories.

Desta forma, garantimos que o domínio fique isolado do restante da aplicação e que só tenha classes com regras de negócio, sem se preocupar com detalhes de implementação que irão ficar em outras camadas.

Considerações Finais

Os Building Blocks são patterns poderosos para nos auxiliar a construir um Domain Model que satisfaça as necessidades do Domain.

Seja pragmático no seu uso, saber não é dever. Cada pattern tem sua utilidade na construção do Domain Model e eles devem ser usados à medida que as necessidades surgem, não antes. Deixe o domínio emergir naturalmente, já que a tendência é começar simples e ir ficando complexo com o tempo. Essas ocasiões são perfeitas para rever o que foi implementado e refatorar o que for necessário.

Pratique! Criei um projeto para exercitar esses patterns e aproveite para se aprofundar mais em cada um conforme as dúvidas surgirem. É nessas horas que você realmente entende o problema que cada um resolve e o seu valor como pattern.

Referências

Discussion

pic
Editor guide