Ao utilizar Domain Driven Design podemos pretender que todas as regras de domínio existam codificadas nas classes que pertencem ao domínio ao invés de espalhar regras de domínio por várias classes em várias camadas do sistema. A principio, não queremos ter lógicas espalhadas por controladores ou serviços de aplicação ou quaisquer outras classes que não pertençam ao domínio.
Lendo na internet encontrei um conjunto de páginas que abordam este tema [1, 2, 3]. Segundo o autor, esta propriedade se chama: Completude . O Modelo de Domínio é Completo se todas as regras de domínio estão escritas apenas em classes do Modelo de Domínio.
Esta é uma tarefa mais árdua do que parece e exige disciplina para não cairmos na tentação de colocar uma regra fora das classes de domínio. Por exemplo, quando realizamos alguma verificação num controlador ou em serviço de aplicação, sem envolver o modelo de domínio, estamos fragmentando o domínio, deixando lógicas que pertencem a ele fora dele. Isto é ruim porque afeta o controle que o domínio tem das regras e a portabilidade dessas regras para outras aplicações que usem o mesmo domínio. Afeta também a capacidade de modificarmos as regras simultaneamente e de forma coerente, pois não sabemos exatamente onde todas as regras estão.
Ao introduzirmos o conceito de domínio e seguir os preceitos do Domain Driven Design é porque queremos concentrar as lógicas em uma só camada. Queremos um modelo completo.
Contudo informações de estado não podem ficar guardadas dentro do domínio a menos que sejam constantes. Mas se todas as informações são constantes talvez não temos um domínio muito útil, não pelo menos em sistema empresariais. Então, é comum utilizarmos o conceito de Repositório para guardar o estado e por baixo dos panos persisti-lo de alguma forma – normalmente num banco de dados.
Segundo o mesmo autor incorremos na degradação de uma outra propriedade do domínio: a Pureza. O domínio é puro se não faz chamadas as outros processos externos a ele. Mais concretamente [2] se não existem inputs ou outputs escondidos durante o funcionamento das lógicas de domínio. Isto pode ser traduzido como: um domínio puro é aquele que não depende de contratos implementados fora do domínio.
A Pureza é fácil de obter de volta, lendo os dados que são necessários fora da classe de domínio e passando esses dados aos objetos de Domínio para realizar as operações. Por exemplo, se queremos verificar que uma nova empresa não está já cadastrada no sistema, utilizando um Repositório simplesmente delegamos para que o repositório verifique se existe uma empresa com um mesmo identificador, como o CNPJ. Contudo, no modelo puro, teríamos que ler todas as empresas persistidas e passá-las para o objeto de domínio que iria comparar com a empresa em causa e verificar se é repetido, por exemplo, usando um método.
O trade-off é claro aqui. Para termos um modelo puro teríamos – a principio – um problema de performance. Digo “a principio” porque existem alguns padrões que poderíamos usar para dar a ilusão de que estamos lendo tudo do banco fora do domínio, mas de fato só ler o que precisamos. Contudo, estes padrões são mais fáceis de aplicar em certas linguagens que em outras, o que em geral nos deixa de volta com o mesmo problema: ter um modelo puro implica ter uma performance baixa.
Contudo isto também não significa que devemos jogar fora a tentativa de manter o domínio puro. Há casos e casos.
O autor segue então argumentando que a fragmentação pode acontecer por motivos de performance e que portanto existem forças que poderiam nos fazer escolher um domínio não completo para obter performance.
Ele concluí entendendo que existe uma tricotomia Pureza-Performance-Complitude e a coloca no mesmo patamar do Teorema CAP e até lhe chama de trilema – um dilema a três.
Finalmente, o autor conclui que devemos preferir um modelo puro a um modelo completo. Ou seja, um modelo que não depende de interfaces implementadas por outros, mas que por causa disso, é fragmentado. Num segundo artigo [2] ele explica como manter a pureza do domino, quando, por exemplo, ele depende do tempo corrente. Fica claro que a aplicação destes conceitos é caso a caso.
O ponto, e meu objetivo com este artigo é reler esta tricotomia à luz da arquitetura multidimensional que abordei antes [5, 6, 7].
À luz da arquitetura multidimensional ou até mesmos das versões mais simples, das arquitetura clean e de cebola, o domínio ocupa o centro dos camadas concêntricas. Isto acontece porque explicitamente queremos que todas as outras camadas dependam do domínio, mas que a camada de domínio não dependa de nenhuma outra camada . Portanto, queremos, por design, um modelo de domínio completo, sem fragmentação. Todas as regras de domínio estão no domínio. É isto que o torna consistente e portátil.
Na Arquitetura multidimensional, nem todas as regras são regras de domínio, e portanto, há decisões e algoritmos que não dependem ou invocam o domínio. Por exemplo, segurança. Esses casos não são relevantes para este artigo pois são cuidados pela camada de Aplicação, fora do Domínio. Nos interessam apenas as regras de domínio.
Todas as arquiteturas planas implicam conversão de dados entre camadas. Isto pode ser visto, em uma análise superficial, com um problema. Afinal estamos transportando os mesmos dados. Correto, mas não com as mesmas operações. Por exemplo, se obtemos uma data no controlador como uma String, podemos até levá-la até à camada de persistência como uma String, mas no momento em que precisamos operar sobre essa data para calcular quando tempo passou de ou até um ponto de referencia, a classe String não nos permite isso , e com razão. Por outro lado, a classe String
nos permite concatenar nossa data com outras informações; uma operação que a principio não nos é favorável na hora de persistir ou até na hora calcular intervalos de tempo. Por tudo isso, convertemos a String
recebida pelo controlador para um objeto que denote corretamente não apenas os dados, mas também as operações sobe esses mesmos dados, de forma coerente, como por exemplo LocalDate
em Java.
Isto significa, também, que em uma arquitetura plana o domínio depende apenas de classes do próprio domínio e toda a informação coletada de outra fonte tem que ser convertida para as classes de domínio. Isto, do ponto de vista da pureza e completude é ótimo pois garantimos que não há dependências externas e portanto não há como existirem lógicas de domínio definidas fora do modelo de domínio.
Por outro lado, temos a persistência dos estado do domínio. Para domínios simples, esta persistência pode ser transiente, ou seja, todos os dados são lidos e informados ao domínio que os manipula durante a execução do sistema e depois todos os dados são gravados de volta no fim da execução. Por exemplo, em um programa de edição de texto. Todo o texto é lido para memória e editado sem afetar o arquivo original a menos que o usuário assim o queira.
Contudo, em sistema empresariais, multi-usuário o estado nunca é transiente. Precisamos, portanto, de contato permanente entre o modelo de domínio e o estado do domínio. Para alcançar isto, usamos Repositórios. Tanto dentro do DDD, como de qualquer arquitetura plana, o uso de repositórios se faz necessário, não por motivos de performance, mas por motivos de partilha de estado. Sim, é claro que a performance acompanha esta questão, mas não é a sua causa. Quando usamos um repositório não é porque queremos ter um sistema rápido, é porque queremos ter um sistema persistente. O que torna a persistência rápida ou não, é o que usarmos para a implementar. Do ponto de vista do modelo de domínio que usa um repositório para acessar e modificar o estado do domínio, propriedades como as da sigla ACID – Atômico, Consistente, Isolado, Durável – são mais importantes que a velocidade.
Portanto, logo na saída qualquer arquitetura plana, e a arquitetura multidimensional em particular definem um modelo de domínio não puro, mas completo. Damos preferência explicita ao uso do design pattern Repository pois sabemos , dentro destas arquitetura, que não há como fugir da necessidade de delegar todas as operações CRUDL ( Create, Retrive, Update, Delete, List) para um banco de dados.
Estabelecemos que precisamos abrir mão da pureza no que trata à implementação.
Alguns autores argumentam que a pesquisa em si, a forma como escrevemos a nossa query ao banco depende dos conceitos do domínio e que, portanto, ao escrevermos essas regras em outra camada onde implementamos os repositórios, estamos fragmentando o domínio. Sou sensível a este argumento, mas no fim de contas o domínio só estabelece um contrato do que é necessário ser encontrado, e não estabelece como será encontrado. Nas arquiteturas planas em geral, a implementação do repositório depende mais do modelo de persistência do que do modelo de domínio. E sim, não têm que ser, e numa arquitetura plana, definitivamente, não são o mesmo.
O caso do acesso à informação de data e hora correntes dados no segundo artigo [2] em que o autor aconselha a passar o data e hora correntes como parâmetro visa maximizar a pureza do domínio. Poderíamos fazer o domínio depender de algum serviço implementado fora do domínio onde obteríamos a data e hora corrente, mas existem vantagens em simplesmente passar o dado como um parâmetro. A principal delas é que isso aumenta a testabilidade.
O Modelo de Domínio é tão mais simples de testar quanto mais puro for. Todos sabemos que testar um sistema em que temos que simular o estado do banco de dados com dados faz-de-conta (mock) é normalmente uma tarefa chata e árdua. O estado que é correto para um teste, não é para outro e ter um banco de dados de teste envolve, normalmente, uma infraestrutura complexa. Mas se o domínio é puro não há dependências externas e o que facilita os testes.
O Modelo de Domínio também é mais simples de testar quanto mais ele for completo. Afinal se todas as regras estão em uma camada temos a certeza que o sistema está realizando as ações corretas do ponto de vista das regras de domínio. Portanto, uma maior completude também implica em uma maior testabilidade.
Podemos resumir da seguinte forma
Consistência | Testabilidade | Persistência | Performance | |
(maior) Pureza | maior | maior | mais complexa | menor |
(maior) Completude | maior | maior | mais simples | menor |
Do quadro acima deve ser claro que devemos sempre almejar um Modelo de Domínio Puro e Completo. Isso permite maior consistência – todas as regras estão em um lugar só – e testabilidade – só precisamos testar uma camada. Mas, sabemos que isto não inteiramente possível no que tange a Persistência. Aqui, a bem da performance e até da simplicidade, temos que abrir mão da pureza. Ao introduzir o Repositório como um contrato – uma interface- do domínio abrimos um porta muito bem definida de como relaxar as restrições de pureza e completude. Podemos dizer até que, estamos apenas relaxando a pureza e mantendo a completude já que todas as regras são definidas por meio do contrato da interface que está no domínio.
Existe um outro ponto do DDD e que é de muita valia nas arquiteturas planas que é o uso de Eventos de Domínio. Eventos de Domínio podem ser recebidos fora do domínio para iniciar ações, mas também podem ser ouvidos por outros contextos de domínio para realizar ações. Esta é uma outra forma em que o domínio pode não ser puro, já que injetamos um distribuidor de eventos (Event Bus) que será usado pelas classes de domínio, mas implementado em outra camada.
Já a completude não é afetada pelo uso de eventos de domínio a menos que o domínio receba e trate eventos que ele mesmo lança. Isto pode afetar a completude no sentido que estamos estabelecendo duas metades da mesma regra. Uma que é executada antes do evento ser lançado e uma que é executada depois do evento ser recebido. Neste caso, porque a recepção depende do Event Bus que é externo, pode acontecer o domínio não receber o evento e a segunda parte não ser realizada. Por outro lado, pode levar a que a segunda parte da regra seja realizada por um objeto fora do domínio, criando assim uma fragmentação.
Olhando para os padrões utilizados em DDD podemos dizer que em geral é bem sabido e resolvido o problema de quando e como abrir mão da pureza e da completude. Temos o padrão Repository para lidar com abrir mão da pureza relacionada à persistência e o padrão Domain Event para lidar com a relação entre contexto de domínio, que pode ser abusada para violar a completude do domínio.
Todas as arquitetura planas e em particular a arquitetura multidimensional aceitam e abraçam estes pontos e os tomam em consideração no seu design, ao mesmo tempo que limitam para que não existam outras formas de driblar a pureza e a completude do domínio tornando o sistema mais fácil de manter e alterar.
Então, de forma diferente do autor, não considero que há um dilema real. Devemos sempre manter a completude e pureza do domínio pois as únicas exceções a essa regra já são bem conhecidas e resolvidas.