Do que se trata o artigo: O Domain-Driven Design é a reunião de um conjunto de princípios e práticas que surgiu com o intuito de atenuar problemas que atormentam desenvolvedores há algum tempo. Não se trata de nenhum novo conceito, mas sim uma espécie de “guia” para a aplicação de práticas já existentes de forma a melhorar o processo de desenvolvimento.
Para que serve: Através de diversos princípios e padrões de projeto, o Domain-Driven Design visa ajudar equipes de desenvolvimento a
entender melhor o contexto dos projetos, permitindo assim utilizar esse conhecimento para gerar um produto final com mais qualidade e satisfação ao cliente.
Em que situação o tema é útil: O Domain-Driven Design apresenta um conjunto de valores e práticas que têm como objetivo auxiliar
desenvolvedores a despender mais tempo no que realmente importa, que é o domínio de uma aplicação. Quanto mais complexo o domínio, mais retorno a aplicação do DDD trará.
Resumo do DevMan: Muito provavelmente você já viu a sigla DDD estampada em algum site ou blog de tecnologia ultimamente. Este artigo tem como objetivo dar um embasamento sobre o Domain-Driven Design, tão comentado atualmente. O principal objetivo é mostrar as ideias e princípios básicos, mostrando pequenos trechos de código de alguns padrões para exemplificar a implementação dessas ideias no código de nossas aplicações.
Para quem nunca ouviu falar sobre Domain-Driven Design, é importante ressaltar que não se trata de um novo framework nem uma nova linguagem de programação, mas sim de uma nova disciplina ou abordagem (alguns diriam até um novo paradigma) para o desenvolvimento de software, ou seja, uma série de ideias e princípios que ao serem aplicados no processo de desenvolvimento de software podem simplificar o mesmo e agregar mais valor ao produto final.
O termo Domain-Driven Design foi criado por Eric Evans em seu livro entitulado “Domain-Driven Design: Tackling Complexity in the Heart of Software”, onde ele fala extensivamente sobre esses princípios e como eles podem ser aplicados e suportados em código, mostrando diversos padrões de projeto de forma detalhada e suas ideias para um melhor desenvolvimento de software após duas décadas percebendo os mesmos problemas.
Mas afinal, o que é DDD?
A principal ideia do DDD é a de que o mais importante em um software não é o seu código, nem sua arquitetura, nem a tecnologia sobre a qual foi desenvolvido, mas sim o problema que o mesmo se propõe a resolver, ou em outras palavras, a regra de negócio. Ela é a razão do software existir, por isso deve receber o máximo de tempo e atenção possíveis. Em praticamente todos os projetos de software, a complexidade não está localizada nos aspectos técnicos, mas sim no negócio, na atividade que é exercida pelo cliente ou problema que o mesmo possui. Como já diz o título do livro de Eric Evans, esse é o “coração”, o ponto central de qualquer aplicação, portanto todo o resto deve ser trabalhado de forma que este “coração” seja entendido e concebido da melhor forma possível.
Vamos utilizar um exemplo para tentar clarificar essa ideia: imagine que o mais novo cliente de sua empresa é um call center e você precisa desenvolver um software que controle todos os processos do mesmo. Mas quem conhece os processos de um call center? Seu gerente? Os desenvolvedores? Com certeza não. Todos estes provavelmente já foram atendidos por um, talvez tenham ideias vagas sobre o funcionamento, mas ninguém possui conhecimento suficiente para guiar o desenvolvimento desse projeto. Quem realmente conhece o negócio são os atendentes, supervisores e demais funcionários desse call center. É deles que esse conhecimento deve ser extraído, de maneira que a equipe do projeto possa se alinhar com seus desejos e expectativas.
Após esse exemplo, vamos fazer um paralelo com alguns conceitos do DDD. O funcionamento e processos do call center são considerados o domínio da aplicação. O domínio é o ponto central de qualquer aplicação, é por causa dele que o software é criado afinal de contas. Reforçando o que já foi falado anteriormente, é o domínio que concentra a maior parte da complexidade de quase todos os projetos de software. O que muitas vezes acontece é um foco excessivo em aspectos técnicos (o velho exemplo da equipe que está “ansiosa” por aplicar a mais nova versão de determinado framework) em detrimento de um conhecimento mais aprofundado no domínio. O grande ponto a ser destacado aqui é que para desenvolver uma aplicação de qualidade, que atenda todas as necessidades do cliente de forma satisfatória, não basta apenas ter muito conhecimento técnico e utilizar os melhores frameworks e plataformas, é necessário que a equipe do projeto tenha uma clara compreensão sobre o problema que o software se propõe a resolver, pois é isso que realmente traz valor ao produto final.
Linguagem Ubíqua
Podemos afirmar com alguma certeza que na maioria dos projetos, a equipe de desenvolvimento não possui inicialmente conhecimento sobre o domínio do problema. Esse conhecimento deverá ser adquirido de pessoas especialistas nesse domínio, que geralmente são os clientes (em alguns casos pode ser um membro da própria equipe – no exemplo do call center são os atendentes, supervisores etc.), desde o início do projeto. A comunicação entre equipe e especialistas do domínio deve ser constante, de forma que o conhecimento seja nivelado o quanto antes. Para que isso possa ocorrer, é necessário que tanto desenvolvedores como os especialistas utilizem a mesma linguagem, com o objetivo de facilitar a comunicação e o entendimento entre todos. Vamos para outro exemplo: imagine um projeto de um sistema de publicação de notas para uma escola, sendo que o especialista com quem você está conversando é um supervisor dos professores, responsável por controlar todo o processo de avaliação da instituição. Essa pessoa possui muito conhecimento sobre o domínio do problema, ou seja, sobre todo o processo de avaliação, como os professores são requisitados a fornecer as notas e uma série de outros detalhes. Por outro lado, esse especialista não possui conhecimento técnico algum para falar sobre classes e design patterns, por exemplo. Imagine a seguinte situação: o especialista comenta que para acessar o sistema os professores devem utilizar o mesmo usuário e senha dos laboratórios de informática da escola. Como você sabe que a autenticação é feita via Active Directory, você o tranquiliza: “não se preocupe, o namespace System.DirectoryServices pode nos ajudar neste ponto”. Imagine a cara da pessoa ao ouvir isso. Certamente ela não terá a menor ideia do que você acabou de falar. Esse problema também pode ocorrer no sentido contrário, com o especialista falando sobre conceitos nunca antes vistos pela equipe. Esse exemplo mostra muito bem a necessidade da criação de uma linguagem comum, conhecida por todos os envolvidos no projeto. A linguagem ubíqua (do inglês Ubiquitous Language, traduzida também para o português como “linguagem difundida” ou “linguagem onipresente”) é o “vocabulário” do projeto, e tem como objetivo viabilizar a comunicação entre desenvolvedores e especialistas de maneira mais natural, para que todos possam contribuir de forma satisfatória nas discussões sobre o domínio. Em alguns casos será necessário abstrair alguns conceitos, mas em outros pode ser que um esclarecimento sobre determinado ponto seja importante para o projeto como um todo (muito provavelmente não é necessário que o especialista entenda o que é o namespace System.DirectoryServices, mas pode ser indispensável que a equipe de desenvolvimento entenda detalhes específicos sobre “EJA”, por exemplo, caso isso seja importante para um melhor entendimento do domínio).
Mas isso tudo não é o papel do analista?
O que ocorre na maioria das vezes é um ambiente onde os desenvolvedores não têm contato com os especialistas do domínio, pois essa é a tarefa do analista. Isso acaba gerando uma distância muito grande entre os desenvolvedores e o contexto do problema, além de frequentemente ocasionar o problema do “telefone sem fio”, onde a informação acaba fatalmente chegando distorcida ao interlocutor. Quem já estudou metodologias ágeis notará uma grande semelhança com um dos princípios do Manifesto Ágil, que diz que especialistas do negócio e desenvolvedores devem trabalhar juntos diariamente no projeto, de forma que o conhecimento seja difundido entre todos os membros.
Nota do DevMan:Manifesto Ágil
O Manifesto Ágil pode ser encontrado em www.agilemanifesto.org. A versão em português está disponível em www.manifestoagil.com.br.
O fato é que tendo um conhecimento aprofundado no domínio da aplicação, os desenvolvedores têm a possibilidade de participar muito mais ativamente do projeto, seja trabalhando junto na concepção do modelo ou desenvolvendo código de qualidade, evitando retrabalho.
O Modelo
Mas nem tudo são flores. Um domínio não é algo facilmente transformável em código, pois consiste em uma série de ideias e conceitos do mundo real. Para que seja possível gerar código que atenda as necessidades de determinado domínio é preciso uma camada intermediária, uma abstração entre a regra do negócio e o código, até mesmo para auxiliar os envolvidos a ter um melhor entendimento sobre o domínio em questão. Essa “camada” é o modelo, que tem como principal objetivo organizar toda a informação obtida dos especialistas, atuando como um guia para a arquitetura e o código da aplicação e, ao mesmo tempo, representando de maneira correta os conceitos do domínio. Este é o grande desafio dos desenvolvedores, criar um modelo que reflita os conceitos do domínio de forma clara que também seja útil para o desenvolvimento. Se o modelo for de difícil implementação ou se não representar a realidade, ele fatalmente não será utilizado e perderá o seu valor.
Muito bem, já vimos as principais ideias pregadas pelo DDD, agora é hora de baixar um pouco o nível e ver as principais práticas sugeridas para a criação do modelo e da arquitetura de uma aplicação. A Figura 1 mostra um mapa com esses conceitos, como mostrado no livro de Eric Evans. A Figura 1 é um exemplo interessante de modelo. Nesse caso, o domínio sendo modelado é o do próprio DDD.
Arquitetura em Camadas
Para que possamos atingir o principal objetivo do DDD, que é focar no domínio da aplicação, é necessário que a arquitetura da mesma seja pensada de forma a permitir essa distinção. Pensando nisso é que o DDD sugere uma arquitetura padrão (Figura 2), que tem como objetivo principal separar o modelo do domínio da tecnologia em si, facilitando assim eventuais mudanças nas regras de negócio da aplicação.
Muitas aplicações possuem regras de negócio espalhadas por todo o código, desde stored procedures na base de dados até JavaScripts na interface do usuário. O grande problema é que alterar alguma dessas regras se torna um verdadeiro pesadelo, uma vez que o desenvolvedor precisará literalmente rastrear todos os locais onde ela deve ser modificada, efetuar as alterações propriamente ditas, e por fim planejar a instalação de todos os componentes modificados.
Para evitar esse tipo de problema, devemos isolar todo o código responsável por controlar o domínio de maneira que ele tenha uma única responsabilidade: implementar regras de negócio. Essa camada deve ser o principal local modificado quando alguma regra da aplicação tiver que ser alterada. Vejamos cada camada:
- Domain Layer: Como já comentado, essa camada concentra toda a regra de negócio da aplicação. Essa deve ser a sua única preocupação, delegando qualquer outro tipo de atividade para as demais camadas;
- Infrastructure Layer: É a camada de mais baixo nível, responsável por prover serviços como persistência e envio de email, ou seja, dar o suporte tecnológico para as demais camadas;
- Application Layer: Coordena as atividades da aplicação, porém, não contém regras de negócio. Pode, por exemplo, manter um estado de determinada transação. Essa é a camada mais discutida, pois dependendo do domínio da aplicação, ela pode não ser necessária.
- User Interface Layer: Responsável pela interação com o usuário, seja interpretando comandos ou exibindo informação para o mesmo.
Veremos agora os principais padrões.
Nota do DevMan
A Figura 1 mostra que Model-Driven Design e Smart UI são mutuamente exclusivos. Pois bem, Smart UI é na verdade um “anti-pattern” que infelizmente é bastante utilizado. A receita para implementar esse padrão é criar a interface do usuário e então colocar todo o código, desde regras de negócio até o envio de email, dentro dos eventos disparados por essa interface. As razões para essa práticas são várias, mas as principais são a “facilidade” de desenvolver aplicações dessa maneira, pois mesmo um iniciante pode abrir o Visual Studio e arrastar diversos controles para um formulário, e o “resultado imediato”, pois é muito rápido implementar uma aplicação simples dessa forma. Mas no momento que alguma regra de negócio for alterada ou algum tipo de manutenção for necessária, o preço pela “agilidade” inicial será cobrado.
Entities, Value Objects, Services e Aggregates
Entities são objetos identificados por sua identidade e não por seus atributos. O exemplo mais simples é uma pessoa. A representação de uma pessoa em software geralmente conta com atributos como nome, data de nascimento, CPF, RG etc. Porém, como podemos distinguir cada pessoa unicamente? Pelo nome e data de nascimento com certeza não, pois muitas pessoas nascem em um mesmo dia e podem existir pessoas com o mesmo nome também. CPF e RG podem ser usados, se levarmos em conta apenas cidadãos brasileiros. A principal ideia é que cada pessoa deve ser única, não sendo possível criar uma pessoa igual apenas “copiando” os atributos de outra.
Por outro lado, Value Objects são objetos que não possuem uma identidade definida, eles são definidos apenas por seus atributos. Para o domínio, não é importante que sejam identificados. O que importa é somente o valor que “carregam”.
Nota do DevMan
Como distinguir Entities e Value Objects? Uma técnica é perguntar se o cliente “se importaria” se o objeto fosse outro. Por exemplo, ao ir no mercado e utilizar um carrinho, você não se importa com QUAL carrinho está usando, apenas que o mesmo o ajude a fazer suas compras. Então, nesse domínio (você fazendo compras), o carrinho seria um Value Object, pois não existe a necessidade de identificá-lo unicamente. Mas caso você fosse um fabricante de carrinhos de supermercado, talvez fosse interessante modelá-los como Entities, já que provavelmente seria necessário poder rastrear cada carrinho individualmente para um melhor controle.
Já os serviços são objetos que têm como finalidade prover um comportamento que não se adapta a nenhum Entity ou Value Object, mas que ainda assim é um conceito importante no contexto do domínio. Um típico exemplo é o cálculo de frete. O CEP é utilizado para efetuar esse cálculo, mas essa lógica está além do objetivo do objeto CEP, portanto não é correto que fique no mesmo. Entretanto, esse conceito também não está diretamente relacionado a nenhum outro objeto. O melhor nesse caso é optar por um Service cujo único objetivo é efetuar o cálculo.
Você notará que, devido a sua natureza, Services geralmente terão como nome um verbo (“CalcularFrete”, por exemplo), diferente de Entities e Value Objects, cuja descrição é geralmente um nome (no exemplo mostrado, “CEP”.)
Já um Aggregate é um grupo de objetos que devem ser tratados como um só, cujo agrupamento representa uma unidade maior no contexto do domínio.
Muitas vezes um objeto filho não tem o menor sentido sem o seu “pai”. O exemplo mais claro é o relacionamento de um Item de Pedido com um Pedido. Os itens só existem em razão do pedido, pois sem pedido não é possível ter qualquer tipo de item.
Como esse é um dos conceitos mais complicados, vejamos o exemplo da Figura 3, adaptado do livro de Eric Evans, que possui dois Aggregates, Cliente e Carro.
Perceba que cada Aggregate possui um objeto principal (objetos Cliente e Carro), que são as raízes dos Aggregates (Aggregate Roots). A raiz de um Aggregate é o único objeto do mesmo que é acessível para os demais objetos. É por ele que todos devem navegar a fim de acessar os objetos internos. Por exemplo, o Cliente não possui acesso direto ao Pneu de um Carro. Para acessá-lo, é preciso “navegar” através do objeto pai, que nesse caso é o Carro. O mesmo vale para o Endereço do objeto Cliente.
Padrões: Factory e Repository
O objetivo do padrão Factory, de forma bem simples, é encapsular a lógica de criação de objetos, tirando essa “carga” do objeto em si.
Vamos utilizar novamente o exemplo da Figura 3 para facilitar o entendimento. Para criar um objeto Carro, é preciso ter conhecimento sobre como criar e associar Pneus e Rodas, além de todas as regras e restrições necessárias para o bom funcionamento do próprio Carro. Essa criação pode ser um processo grande, que contém muitas regras que o objeto Carro, durante sua “vida”, jamais precisará utilizar. A utilização de uma Factory nesse caso é justamente para retirar essa lógica do objeto Carro e colocá-la em um objeto especializado em criação de Carros, ou seja, uma Factory. Fazendo um paralelo com o mundo real, o conceito é o mesmo de uma fábrica de automóveis. Carros que saem da linha de produção não precisam saber “se construir”. Existe todo um processo responsável exclusivamente pela fabricação, com o único objetivo de criar um objeto funcional (nesse caso o carro). Uma vez fabricado, o carro funciona normalmente, mas não tem ideia de como foi fabricado.
Nota do Editor
Nesta mesma edição, você encontra um artigo sobre o padrão Factory, em C#.
Voltando ao mundo orientado a objetos, utilizamos Factories quando a criação de objetos envolve um certo nível de complexidade e cuja lógica não necessita ser conhecida pelo objeto em si, pois ela é necessária somente no momento da criação do mesmo. Factories são especialmente úteis para a criação de Aggregates, visto que estes geralmente possuem diversas associações e restrições a serem aplicadas.
O principal design pattern utilizado para a implementação de Factories é o Factory Method. A Figura 4 mostra o diagrama de classes que implementa esse padrão de maneira muito simplificada para a criação de diferentes tipos de carros, Hatch e Sedan.
Perceba que temos classes abstratas tanto para os carros quanto para nossas Factories, pois geralmente teremos certos comportamentos e regras que devem ser aplicados para todos os tipos. No caso da classe Carro, ela fornece a propriedade Placa, que será utilizada por todos, além de ser o tipo retornado por todas as Factories. Já a classe CarroFactory não fornece nada que possa ser herdado pelas demais (apenas o método Cria(), que é abstrato) e poderia ser até uma interface neste caso. Mas como geralmente temos comportamentos genéricos para todas as Factories, a deixaremos como uma classe abstrata. Já as Factories concretas são as responsáveis por realmente criar objetos, pois implementam o método Cria(). São essas classes que conhecem os detalhes de implementação dos objetos sendo criados, portanto qualquer lógica de negócio necessária para a criação deve estar localizada aqui.
Na Listagem 1 temos implementação desse simples exemplo e na Listagem 2 testes para verificar o funcionamento das Factories.
public class Hatch : Carro { } public class Sedan : Carro { } public abstract class CarroFactory { public abstract Carro Cria(); } public class HatchFactory : CarroFactory { public override Carro Cria() { //Aqui vai toda a lógica de criação de carros hatch return new Hatch(); } } public class SedanFactory : CarroFactory { public override Carro Cria() { //Aqui vai toda a lógica de criação de carros sedan return new Sedan(); } }
[TestMethod()] public void SedanFactoryTest() { CarroFactory target = new SedanFactory(); Carro expected = target.Cria(); Assert.AreEqual(typeof(Sedan), expected.GetType()); } [TestMethod()] public void HatchFactoryTest() { CarroFactory target = new HatchFactory(); Carro expected = target.Cria(); Assert.AreEqual(typeof(Hatch), expected.GetType()); }
Além da separação de responsabilidades, esse padrão possibilita decidirmos qual classe será criada em tempo de execução. Por exemplo, poderíamos ter uma classe SedanComGPS herdando a classe Sedan e através de algum parâmetro no método Cria(), a classe SedanFactory poderia decidir se retornaria um objeto Sedan ou um SedanComGPS.
Outro padrão, muito importante no DDD, é o Repository, que abstrai o acesso à fonte de dados, provendo uma interface similar a uma coleção que pode ser facilmente manipulada pelos objetos do domínio, evitando assim que a aplicação seja “Database-Driven”.
Esse padrão é um dos principais fundamentos do DDD, pois é através dele que a camada de domínio fica livre de qualquer código necessário para o acesso à fonte de dados.
O padrão Repository originalmente sugere que tenhamos “filtros” que possam ser criados e, baseados nos critérios definidos nestes filtros, o repositório retorna os objetos que atendem essas especificações. Entretanto, na plataforma .NET temos o LINQ, que simplifica ainda mais o acesso ao repositório, eliminando a necessidade de filtros específicos e possibilitando queries diretas ao mesmo.
Na Listagem 3 temos um exemplo de uma interface para um Repository com suporte a LINQ, através da interface IQueryable. Você pode notar que não temos métodos para retornar dados, apenas para adição, atualização e remoção. Isso elimina a necessidade de diversos métodos iniciados por “Get” (GetById, GetByName) ou filtros específicos, resultando em repositórios mais limpos e ao mesmo tempo mais flexíveis.
public interface IRepository : IQueryable { void Adicionar(T object); void Atualizar(T object); void Remover(T object); }
Conclusão
O principal objetivo deste artigo é apresentar o DDD e explicar suas principais idéias e padrões mais básicos, de forma que o leitor se familiarize com o assunto para então buscar aplicações reais do que foi mostrado. É importante ressaltar que grande parte do que foi mostrado aqui não são conceitos novos, apenas um agrupamento de ideias já existentes.
Provavelmente você sentiu falta de código C# ao longo do texto, isso porque o objetivo principal do artigo é deixar claro o principal objetivo do DDD, que é o foco no entendimento, análise e compreensão de problemas e não na codificação em si. Vejo muitas pessoas que lêem alguns parágrafos sobre DDD e já vão à procura de exemplos de código e como implementar em seus projetos. Como vimos, existem sim algumas práticas de mais “baixo nível” sugeridas pelo DDD, mas o código será apenas uma consequência da correta aplicação dos princípios citados. Reforçando, o valor do software não está no código propriamente dito, mas sim na construção do conhecimento sobre o problema e em como se chegou à conclusão de que determinado código era o melhor para resolver tal problema. De nada adianta apenas implementar o padrão Repository para abstrair o acesso a dados apenas para “utilizar DDD”. Isso será perda de tempo, pois não trará ganho real nem para você nem para o seu cliente.
Enfim, espero que você tenha entendido os princípios do Domain-Driven Design e possa levá-los em conta ao decorrer do desenvolvimento de seus projetos. Você pode chegar à conclusão que alguns pontos citados ao longo não se aplicam ao seu contexto, o que é perfeitamente normal. O DDD geralmente agrega mais valor em softwares de alta complexidade, onde entender bem o domínio é vital para o sucesso dos mesmos. Para quem se interessou no assunto e quiser ir mais a fundo, recomendo a leitura do livro de Eric Evans, que explica em detalhes os conceitos aqui apresentados e muito mais.
Um abraço e até a próxima.
Referências
- Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software, 2003.
- Nilsson, Jimmy. Applying Domain-Driven Design and Patterns: With Examples in C# and .NET, 2006
- McCarthy, Tim. .NET Domain-Driven Design with C#: Problem - Design – Solution, 2008.
- Freeman, Elisabeth; Freeman, Eric; Bates, Bert; Sierra, Kathy. Head First Design Patterns, 2004.
- Domain-Driven Design Quickly - www.infoq.com/minibooks/domain-driven-design-quickly
- Domain-Driven Design Community