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.
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.
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.
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.
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.
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.
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.
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.
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.
Top comments (2)
Obrigado por compartilhar amigo! Ficou muito legal e sucinto!
Ótimo texto. Bastante didático.