Domain-Driven Design, Os building blocks | PARTE 2: Entities e Value Objects

Maicon Heck
CWI Software
Published in
9 min readOct 8, 2018

👉 This article is also available in English on my blog

Na primeira parte dessa série de artigos sobre Domain-Driven Design, eu falei sobre as características de um modelo efetivo, sobre a construção da linguagem ubíqua, e sobre o valor imprescindível de mantermos uma interação direta entre especialistas de domínio e desenvolvedores, para termos uma modelagem que realmente traga valor para o negócio. Se você não leu, sugiro iniciar por lá:

Com um time que tenha esse mindset, e uma dose de obsessão em entender profundamente o domínio (knowledge crunching), amparados por um método ágil, aí então podemos implementar DDD.

Nesse exemplo vamos modelar um sistema de automação de marketing. Eu optei por esse domínio, pois tenho uma domain expert que mora comigo. 🙋‍

Minha esposa já trabalhou com duas plataformas de automação de marketing, entre elas o mautic, que é um sistema de automação de marketing open source muito popular nesse mercado.

Além disso, esse domínio me pareceu um bom desafio para simular o processo de knowledge crunching e a formação da linguagem ubíqua, pois não conhecia nada a respeito dele. 🤔

Após várias perguntas e um brainstorming eu pude desenhar esse modelo inicial:

O modelo inicial (PS.: Eu desenho tão mal quanto tiro fotos 😳)

EU: Para que servem / qual a função dos sistemas de automação de marketing?

ELA: Servem para capturar e centralizar os leads (possíveis clientes) em um só lugar.
EU: Como são capturados os leads?

ELA: De 3 formas:

  • Através da importação de uma base de clientes já existentes (excel).
  • Através do cadastro manual.
  • Através de um formulário gerado, onde você solicita ao visitante que informe, entre outros dados, o e-mail. Ao fazê-lo esse formulário será enviado ao mautic.

EU: Entendi. E quais são os dados do lead que são capturados através dessas 3 formas?

ELA: Depende da forma de captura.

EU: Certo, mas quais são as informações mínimas essenciais para que um lead exista?

ELA: O mínimo é o nome e o e-mail, que aliás, são duas informações valiosas para conversão de um cliente (venda). 💲

EU: E quais são todos os dados possíveis que um lead pode ter?

ELA: [após pensar por um tempo…]: Nome, e-mail, segmento(s), telefone, endereço, sexo, data de nascimento e empresa.

EU: [após refletir sobre os dados…] O que são os segmentos e para que eles servem?

ELA: Os segmentos são formas de classificar os leads que posteriormente serão usados nas campanhas.

EU: Há um termo novo aqui: campanhas — o que são?

ELA: As campanhas são sequências de e-mails disparados para os leads. Por exemplo: eu posso ter a campanha “Download de e-book: Como acelerar as vendas” ou “Inscrição para Webinar ao vivo: Acelere sua startup”. Ao criar uma campanha eu configuro uma sequência de e-mails com data e horário programados e ainda posso colocar condições nesses e-mails…

Nosso diálogo seguiu assim por um bom tempo, onde eu fui apreendendo sobre o domínio, e juntos fomos criando o modelo inicial.

Mas vou interrompê-lo para não me estender mais. Além disso, com essa parte do domínio, já temos bastante código para começar a implementar os building blocks do DDD. 🚧

Entity

Por definição, uma entidade (entity) é um objeto que possui identidade, e essa identidade persiste durante os ciclos de vida desse objeto.

Todo desenvolvedor sabe o que é uma entidade. Mas será que todos estão modelando entidades corretamente?

Analisando o modelo identifiquei as entidades: Lead e Segments.

A baixo temos o código da entidade Lead:

Mas olhe para o código acima. Qual o problema com essa entidade? Bem, vários. 😱

💩 Ela não informa os dados que necessita para que possamos criar uma instância válida (tem o construtor default).

💩 Os tipos por referência não estão sendo inicializados, portanto, ao acessá-los teremos uma exceção:

var lead = new Lead();
lead.Segments // Teremos NullReferenceException aqui.💩

💩 Não tem encapsulamento! Todas as propriedades são públicas, ao invés de haver uma interface pública e bem definida para acessá-las.

💩 É toda composta por tipos primitivos, não tem inteligência e não faz nenhum reúso.

É uma típica entidade anêmica, sem conhecimento e, portanto, sem valor para o modelo. O conjunto de entidades nesse formato, entre outros code smells, caracteriza um modelo anêmico, que é o contrário do que queremos.

Então temos esses 4 problemas. Vamos resolver um a um:

♻️ Resolvendo o primeiro problema (definindo um construtor que informe os dados necessários):

Agora Lead informa o que precisa para que uma instância dela seja criada (nome e e-mail).

♻️ Agora vamos resolver o segundo problema (inicializando Segments):

Inicializando Segments para evitar uma possível exceção.

♻️ Resolvendo o terceiro problema (a falta de encapsulamento):

Resolvendo o terceiro problema (falta de encapsulamento).

Vamos analisar o que estamos fazendo acima.

Estamos protegendo os dados através do encapsulamento (note o modificador de visibilidade private).

Para proteger os Segments de serem atualizados por fora da nossa interface pública, precisamos usar a interface IReadOnlyList.

Se usarmos ICollectionou IList, apesar da propriedade ser private, seria possível acessar o método Add() ou Remove()e adicionar ou remover itens por fora da interface pública, quebrando assim o encapsulamento:

var lead = new Lead(…);
lead.Segments.Add(…);
lead.Segments.Remove(…);

A interface IReadOnlyListnão possui os métodos Add()e Remove(), sendo assim, uma instância de Lead só pode ter seus Segments atualizados através da sua interface pública.

Note o papel do encapsulamento, não apenas provendo segurança aos dados, mas também agregando conhecimento ao domínio.

Estamos ocultando do cliente da classe Lead, a complexidade dos dados e operações internas à ela, e expondo apenas a sua interface pública, que é o que interessa para ele. Fizemos isso através do método CompleteInfo(...)

A propósito, o nome do método CompleteInfo(...), foi extraído da linguagem utilizada pelo profissional de marketing que realiza esta operação — i.e. assim que possível, ele completa a informação do Lead com os dados que ele não tinha quando obteve o Lead. Portanto, completar a informação (Complete Info) faz parte da linguagem ubíqua deste domínio.

Ainda que para o desenvolvedor(a), isso seja apenas um Update ou Edit, DDD é sobre criar modelos que reflitam a linguagem do domínio, evitando assim traduções entre os developers e os domain experts, e consequentemente, criando modelos que refletem o negócio através do código.

Perceba que agora deixamos claro como completar a informação do Lead:

void CompleteInfo(string phoneNumber, string address, bool gender, DateTime birthDate);

Eric Evans chama isso de interfaces que revelam a intenção, ou seja, que deixam claro para o cliente da classe como usá-la.

Ele adverte que se a interface não disser ao cliente o que ele precisa saber para usar o objeto, ele terá que se aprofundar na implementação da classe para entendê-la. Então, o valor do encapsulamento será perdido.

EVANS (2004)

Ao expor apenas uma interface pública, bem definida, nós simplificamos o uso da classe, pois deixamos claro como um Lead deve ser construído e atualizado.

Assim evitamos a sobrecarga de informação que estamos expondo à mente da quem precisa trabalhar com essa classe. 😓

Do contrário, o desenvolvedor teria que descobrir: “o que 🤬😖 eu preciso para ter um Lead válido, já que não há um construtor que me informe isso, e todas as propriedades são públicas?”

Lembre-se: OOP é sobre diminuir a complexidade!

Agora nossa entidade está, sem dúvida, bem mais em conformidade com o modelo. Porém, ainda há um problema a ser resolvido, lembra?

💩 É toda composta por tipos primitivos, não tem inteligência e não faz nenhum reúso.

Olhe novamente para a última versão que temos do código. Perceba que estamos fazendo uso excessivo de tipos primitivos.

string Name
string Email
string PhoneNumber
string Address
bool Gender

Excetuando-se Segments, estamos usando somente tipos primitivos (string e bool). E isso não agrega nenhum valor ao modelo, pois apenas representa uma estrutura de dados. Por exemplo, como eu sei se Email é válido?

E quanto a PhoneNumber e Address, qual o formato deles?

Confiando apenas em tipos primitivos, podemos ter um Lead ridiculamente válido, assim:

var lead = new Lead(name: "", email: "(51) 9999-9999");lead.CompleteInfo(phoneNumber: "Não lembro", address: "Que tipo de endereço?", false, null);

Então, vamos refatorar a classe Lead novamente, dessa vez para substituir os tipos primitivos por Value Objects.

Value Object

Objetos de valor (Value Objects), são objetos que representam um aspecto descritivo do domínio, mas ao contrário das entidades eles não têm identidade.

Existem alguns benéficos extra ao usarmos um Value Object:

Por não possuírem identidade, se implementados corretamente, podem contribuir na performance, já que a mesma instância de um Value Object pode ser compartilhada entre várias instâncias de uma Entity. Por exemplo, todas instâncias de Leads que moram no mesmo lugar poderiam compartilhar a mesma instância de Address.

Pela mesma razão, os Value Objects podem implementar funções livres de efeitos colaterais.

Para o nosso caso, porém, o interesse inicial nos Value Objects, é para agregarmos mais riqueza ao nosso modelo, e ao mesmo tempo torná-lo testável.

Então vamos substituir os tipos primitivos (Name, Email, PhoneNumber, Address) pelos respectivos Value Objects, conforme o código a baixo:

Não se preocupe com os métodos Guard.Against(...), eu irei explicá-los depois.

O Value Object Name
O Value Object Email
O Value Object PhoneNumber
O Value Object Address

Os métodos Guard.Against(...) são Guard Clauses que servem para validar os Value Objects . Através das Guard Clauses, nós criamos pré-condições que devem ser satisfeitas para que possamos avançar para as próximas instruções do código. Caso essas pré-condições não sejam satisfeitas, haverá uma exceção.

Sendo assim, se tentarmos, por exemplo, inicializar um Email com um valor inválido, teremos uma exceção:

var email = new Email("foo@.com");
// FormatException: Invalid e-mail address: foo@.com.

Nos próximos artigos dessa série, conforme o nosso modelo for evoluindo, eu vou demonstrar o uso do CanExecute Pattern e do Notification Pattern, que em conjunto, nos permitem oferecer ao caller um meio de testar as operações antes de executá-las, e no caso de não poder executá-las — por violarem o contrato, retornar os erros ao client.

Como você pode ver, o nosso modelo melhorou bastante ao substituir os tipos primitivos por Value Objects.

As regras (que por enquanto são apenas validações) estão onde deveriam estar: no domínio. E elas pertencem aos seus respectivos objetos, que não são mais apenas uma estrutura de dados sem valor.

E falando em regras, onde há regras, há testes:

Os testes do Name
Os testes do Email
Os testes do PhoneNumber
Os testes do Address

E para concluir, segue a nossa classe Lead refatorada para conter os Value Objects, e seus respectivos testes:

A última versão da classe Lead (até agora)
Os testes da classe Lead

Se você comparar o código dessa versão daLead com a anterior, vai perceber que, estruturalmente elas são bastante semelhantes. Mudaram basicamente apenas os tipos, que antes eram primitivos e sem valor, e agora são Value Objects, com regras de validação bem definidas e cobertas por 50 testes unitários:

Os testes unitários do domínio (até agora)

Naturalmente, ficou um pouco mais trabalhoso inicializar e atualizar um Lead, afinal, agora os construtores do Lead e dos Value Objects deixam explicito àquilo que eles precisam para serem instanciados:

Inicializando e atualizando um Lead

Em compensação, agora o esforço cognitivo diminuiu consideravelmente, pois não precisamos mais ficar tentando adivinhar quais propriedades são requeridas em uma nova instância — nunca tendo certeza, e cada desenvolvedor(a) fazendo de uma forma diferente, o que no final leva a uma serie de bugs — ou seja, é um tradeoff que vale muito a pena.

E se no futuro, houver algum construtor muito complexo, podemos mitigar essa complexidade através do pattern Factory Method.

Conclusão

Há um longo caminho a percorrer para implementar DDD. Mas o DDD pagará várias vezes esse esforço!

Onde essas regras (que por enquanto são apenas validações, depois teremos ainda as regras de negócio) acabariam sendo colocadas se nos orientássemos pela forma data-driven de desenvolver software, onde somos guiados pelo modelo ER ao invés do domínio?

Se quisermos fazer design orientado a objetos, precisamos temporariamente esquecer tabelas. Vamos lembrar delas quando formos implementar a persistência. Repito: quando formos implementar a persistência — E eu vou mostrar como fazer o mapeamento das Entities e ValueObjects para as tabelas, e inclusive como mapear Entities que possuem um relacionamento de herança.

Nós estamos criando um código orientado a objetos, amplamente testado, e mais importante, que reflete a linguagem do domínio, e aqui nós estamos apenas arranhando a superfície — nos próximos artigos desta série vamos modelar Aggregates com Entities complexas, e então vai ficar mais evidente ainda o poder que a OO nos oferece no design do modelo de domínio.

Mas para fazermos isso, antes eu preciso que você entenda mais 3 building blocks essenciais do DDD: Domain, Subdomains e Bounded Contexts — e é isso que eu ensino no próximo artigo:

Referências

Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. 2004.

Vernon, Vaughn. Implementing Domain-Driven Design. 2013.

Maicon Heck
.NET Senior Developer | Software Craftsman | Domain-Driven Design practitioner

blog | github | linkedin | twitter

--

--

Maicon Heck
CWI Software

I'm a software crafter. Through DDD I crafted dozens of rich, extensible, testable, and long-lived business domain models.