Atualmente, boa parte dos aplicativos úteis em nossas vidas são considerados complexos. O que muitas vezes deixamos de levar em consideração a respeito deles é a complexidade da lógica de negócios com a qual estamos lidando, o que faz com que o desenvolvimento seja dificultado. Um meio que possuímos para facilitar esse desenvolvimento é através do uso do DDD – Domain-Driven Design (em tradução literal, Design Direcionado ao Domínio). Essa metodologia de design está presente em várias aplicações desenvolvidas atualmente e, como o nome sugere, diz respeito à prepararmos uma aplicação direcionando-nos ao domínio de negócios da mesma.

Esse artigo visa entender o funcionamento do DDD utilizando o .NET Framework. Veremos que o DDD utiliza alguns padrões de design para as aplicações, e que eles são muito úteis na prática. Entretanto, a grande sacada dessa metodologia é a capacidade que ele traz para a modelagem do problema, através do entendimento do mesmo. Ao longo do artigo, traremos uma introdução ao DDD e alguns padrões comuns no desenvolvimento de aplicações com essa metodologia, como os padrões Domain Model e Repository.

O que é o DDD (Domain-Driven Design)

O DDD ao qual nos referimos não tem absolutamente nada a ver com o código de área que discamos quando precisamos fazer uma ligação de longa distância. Dentro do desenvolvimento de software, o DDD é o Domain-Driven Design, e trata-se de uma sigla muito comum no meio. Trata-se de uma metodologia de design de software que tem um foco no que está acontecendo no domínio da aplicação. Em outras palavras, e como o nome sugere, o design é centrado na lógica de negócios (domínio) do software.

A ideia básica do DDD está centrada no conhecimento do problema para o qual o software foi proposto. Na prática, trata-se de uma coleção de padrões e princípios de design que buscam auxiliar o desenvolvedor na tarefa de construir aplicações que reflitam um entendimento do negócio. É a construção do software a partir da modelagem do domínio real como classes de domínio que, por sua vez, possuirão relacionamentos entre elas.

Essa modelagem de domínio encaixa-se perfeitamente no que conhecemos de bancos de dados relacionais e, mais recentemente, de ORM’s (Mapeadores Objeto-Relacionais). Ou seja: o domínio criado utilizando o DDD será uma modelagem bastante simples dos dados que serão armazenados pela aplicação em sua base de dados. Esse modelo, entretanto, necessita de mais alguns detalhes para funcionar corretamente, e é isso que veremos a seguir.

Separando a aplicação em camadas

Uma abordagem muito comum no DDD é a separação em camadas. Essa separação é uma realidade no desenvolvimento de software há algum tempo, mas que só recentemente foi levada a sério, com o aumento da complexidade das aplicações. Relacionando-a especificamente ao DDD, é um conceito que auxilia muito na separação dos problemas, necessária no desenvolvimento DDD. A Figura 1 mostra a separação em camadas comum no DDD.

Separação
em camadas do DDD

Figura 1. Separação em camadas do DDD

Ela traz uma separação dos problemas, algo muito comum no DDD. Esses conceitos encaixam-se em elementos diferentes da aplicação, e por isso são separados dessa forma. Entretanto, é importante ressaltarmos que o DDD não é simplesmente a forma como definimos nosso software: é como preparamos o desenvolvimento como um todo, de uma forma mais ampla. Em outras palavras, o DDD é sobre como pensamos a respeito do software, e não como ele está estruturado.

A Figura 1 trouxe algumas camadas comuns em aplicações DDD. Como o espaço disponível aqui não é suficiente para uma análise em detalhes a respeito do assunto, vamos trazer alguns desses elementos, apenas. Esses elementos estão listados a seguir:


  • Modelo de domínio: Normalmente definido através do padrão Domain Model, essa camada é a base de todo o DDD. É aqui que precisamos definir corretamente nosso modelo de negócios em termos de classes, do contrário teremos problemas posteriores em nosso projeto.
  • Serviços de domínio: Os serviços de domínio fornecem alguns métodos necessários à aplicação, mas que não estão restritos à uma só entidade – como uma classe do modelo de domínio é chamada. Em linhas gerais, contém regras e comportamentos referentes ao Modelo de domínio.
  • Repositórios de dados: Os repositórios de dados são definidos através de um padrão: o padrão Repository. A ideia é que o Modelo de domínio esteja livre de qualquer definição de infraestrutura de dados, fazendo com que ele siga os princípios POCO (Plain Old Common-Runtime Object) e PI (Persistence Ignorance) – termos que são utilizados em conjunto para indicar objetos que não possuem comportamentos (métodos) definidos (entidades).

Os três elementos listados acima são essenciais no desenvolvimento utilizando DDD. Os demais também o são, mas o nível de importância desses três, no que diz respeito ao domínio da aplicação, é maior, e por isso nosso foco estará neles, em especial o Modelo de Domínio e os Repositórios. Na sequência, veremos como esses elementos interagem na prática.

Conhecendo o padrão Domain Model

O padrão Domain Model é, como o nome sugere, um modelo do domínio de nossa aplicação. Como tal, ele é o responsável pela modelagem das classes de domínio de acordo com o que a realidade nos apresenta. Isso faz com que se trate de um dos elementos mais importantes dentro da aplicação, uma vez que todas as operações envolverão, de uma forma ou outra, o domínio. Vamos começar analisando as classes desse domínio.

As classes do modelo de domínio são chamadas de entidades. Essas entidades, por sua vez, possuem relacionamentos com outras entidades. É extremamente importante que as entidades e seus relacionamentos tenham um paralelo na realidade, pois assim será muito mais fácil para o desenvolvedor entender e replicar a lógica do negócio, contendo todos os processos de validação e regras.

As entidades presentes no Domain Model não possuem nenhum conhecimento de como fazer com que seus dados sejam persistentes. A persistência de dados é um conceito importante em uma aplicação, uma vez que evita que haja perda destes, ou seja, faz com que os dados sejam armazenados em algum lugar. Logo, essas entidades são consideradas dentro dos princípios POCO e PI, já mencionados. Outro ponto importante a respeito do modelo de domínio é que não há, necessariamente, uma correspondência entre o modelo de dados (que representa a base de dados) e as entidades do modelo de negócios (Domain Model).

Com esses conceitos, surge uma questão importante: como faremos a persistência dos dados do modelo de domínio? Normalmente é utilizado o padrão Repository, que veremos em detalhes a seguir. Sucintamente, esse padrão é responsável pelo mapeamento da entidade do modelo de domínio para a entidade associada no modelo de dados.

Antes de lidarmos com o padrão Repository, precisamos criar um modelo de negócios. Estaremos utilizando o padrão Domain Model, de modo que o leitor possa entender como é simples, uma vez que conhecemos o negócio, defini-lo.

Vamos começar criando uma solução vazia no Visual Studio, como mostra a Figura 2. Essa solução vazia conterá dois projetos, apenas. Vale ressaltar que, em projetos reais, maiores, normalmente haverá mais projetos, cada um representando uma camada. Aqui, teremos apenas as camadas de Domínio (Domain Model) e Repositório (padrão Repository) representadas. Vamos criar, portanto, esses dois projetos. Ambos são do tipo “Class Library”. A estrutura da solução após a criação desses projetos está mostrada na Figura 3.

Criando
uma solução vazia

Figura 2. Criando uma solução vazia

Estrutura
da solução exemplo

Figura 3. Estrutura da solução exemplo

Nesse momento possuímos apenas dois projetos, sendo ambos do tipo “Class Library”, ou Biblioteca de Classes. Na Figura 3 podemos notar que o primeiro deles (Domínio) está definido como o projeto inicial, a ser executado no momento do build. Entretanto, se tentarmos realizar essa execução, haverá um erro. Isso ocorre porque projetos do tipo “Class Library” não podem ser executados, somente compilados. Portanto, se desejarmos utilizar essas bibliotecas, precisaríamos de um projeto diferente, web ou desktop, que permitisse a utilização dos mesmos. Não criaremos esse projeto nesse exemplo, uma vez que estamos interessados em destrinchar o padrão Domain Model, inicialmente, e não em mostrar o funcionamento de uma aplicação qualquer. Esse projeto seria o equivalente à camada IU, mostrada na Figura 1. As demais camadas apresentadas nessa figura também serão omitidas nesse exemplo.

Outro ponto digno de nota é referente aos relacionamentos entre as camadas. Como queremos manter a camada de domínio (projeto Domínio) livre de qualquer questão de infraestrutura, ela não receberá qualquer referência das outras camadas.

Agora que entendemos a estrutura do projeto, podemos começar a definir o mesmo. Nosso modelo de negócios possui duas classes: Categoria e Produto. Cada categoria pode possuir vários produtos, enquanto um produto somente possuirá uma categoria. Esse relacionamento é bastante simples e serve a nosso propósito. As classes de domínio estão criadas, como mostram as Listagens 1 e 2.

Listagem 1. Classe Categoria

namespace Domínio
  {
      public class Categoria
      {
          // Propriedades
          public int Id_Categoria { get; set; }
          public string Nome { get; set; }
          public string Descricao { get; set; }
   
          public IList<Produto> Produtos { get; set; }
      }
  }

Listagem 2. Classe Produto

namespace Domínio
  {
      public class Produto
      {
          // Propriedades
          public int Id_Produto { get; set; }
          public string Nome { get; set; }
          public string Descricao { get; set; }
          public decimal Preco { get; set; }
          public int Quantidade { get; set; }
   
          public int Categoria { get; set; }
   
          // Métodos
          public bool PodeVender()
          {
              return Quantidade > 0;
          }
   
          public void Vendeu()
          {
              if (PodeVender())
                  Quantidade--;
          }
   
          public void Comprou(int qtde)
          {
              Quantidade += qtde;
          }
      }
  }

A classe Categoria possui a referência a todos os produtos contidos na determinada categoria. Isso é condizente com o relacionamento que as classes de domínio possuem. No mais, apenas representações simples das informações necessárias ao armazenamento de cada categoria.

Já a classe Produto é mais complexa, pois o padrão Domain Model insiste que adicionemos comportamento para nossas classes de domínio. Enquanto Categoria não possuía nenhum comportamento (a exceção dos getters e setters), Produto os possui. Além dos detalhes normais de representação dos dados, bem como a referência à Categoria a qual o produto está vinculado, temos os métodos PodeVender(), Vendeu() e Comprou(), que referem-se à quantidade de produtos disponível para venda. Nesse caso, estamos verificando se os produtos estão disponíveis para venda, vendendo e comprando, respectivamente.

Quando lidamos com o mapeamento objeto-relacional em nossos projetos, é comum que tenhamos classes de domínio sem qualquer definição de comportamento. Isso faz com que não estejamos lidando mais com o padrão Domain Model, e sim com uma variação dele: o Anemic Domain Model, que significa, em uma tradução literal, Modelo de Domínio Anêmico. Nesse caso, é preciso que tenhamos classes de serviço responsáveis pela definição dos comportamentos das classes de domínio. Ambos podem ser utilizados com o DDD, embora seja mais comum vermos a utilização do Domain Model.

Com isso, temos nosso modelo de domínio definido. Entretanto, não há nenhuma persistência de dados, até o momento. Por isso, iremos utilizar o padrão Repository para adicionar essa persistência e permitirmos o acesso a alguma base de dados em nossa aplicação.

Domain Model + Padrão Repository

O fato de que o Domain Model não fornece persistência aos dados de nossa aplicação poderia ser um problema, não fosse a existência do padrão Repository. Os dois padrões costumam ser utilizados em conjunto pelo simples fato de que eles se complementam. O primeiro cria o modelo de domínio sem a menor preocupação a respeito de como os dados serão armazenados e o último toma conta dessa ponte entre o modelo de domínio e a base de dados.

Como podemos ver, a ideia é extremamente simples. Entretanto, a forma como os repositórios de dados será implementada é muito dependente do banco de dados que estamos utilizando. Além disso, também há diferenças para casos em que utilizamos ferramentas de ORM (Object-Relational Mapping – Mapeamento Objeto-Relacional), como o Entity Framework, o LINQ to SQL ou o NHibernate. A ideia básica é que o repositório precisa estar adaptado à essa estrutura.

A ideia básica é que, para cada classe de domínio, tenhamos um repositório de dados. Para o exemplo que estamos criando, teríamos um repositório de Produto e outro de Categoria. Vamos criar esses dois elementos em nosso projeto a seguir. Primeiramente, porém, precisamos realizar a importação das classes de domínio em nosso projeto Repositório (que representa a camada Repositórios da Figura 1). Para isso, basta clicar com o botão direito nas referências do projeto Repositório e adicionar o projeto Domínio, como mostra a Figura 4.

Adicionando
referência à camada Domínio em nosso Repositório

Figura 4. Adicionando referência à camada Domínio em nosso Repositório

Com isso, podemos começar a criação de nosso repositório propriamente dito. O padrão Repository funciona, comumente, através de interfaces. Essas interfaces visam criar uma abstração do repositório em si, apenas para representar as operações que serão feitas nos dados, sem considerar o banco de dados utilizado. Logo, uma mesma interface IProdutoRepository, por exemplo, tem a mesma assinatura para um banco de dados SQL Server ou PostgreSQL.

Vamos criar as interfaces para os repositórios de dados, portanto. Elas estão mostradas nas Listagens 3 e 4. Repare que a nomenclatura utilizada começa com a letra “I”, para indicar que se trata de uma interface, e termina com o sufixo “Repository”. Essa nomenclatura é uma convenção quando utilizamos o padrão Repository. Os métodos assinados nas interfaces são similares para ambos os repositórios, com a diferença do tipo de dados: Categoria e Produto, respectivamente. A ideia é que tenhamos métodos para adicionar, salvar e buscar os dados na base, uma vez que o repositório concreto esteja criado.

Listagem 3. Interface ICategoriaRepository

namespace Repositório
  {
      public interface ICategoriaRepository
      {
          void Adicionar(Categoria categoria); 
          void Salvar(Categoria categoria); 
          IEnumerable<Categoria> ListarTodos(); 
          Categoria BuscarPor(int idCategoria); 
      }
  }

Listagem 4. Interface IProdutoRepository

namespace Repositório
  {
      public interface IProdutoRepository
      {
          void Adicionar(Produto produto);
          void Salvar(Produto produto);
          IEnumerable<Produto> ListarTodos();
          Produto BuscarPor(int idProduto); 
      }
  }

Com isso temos a base para nosso repositório de dados. O repositório concreto irá variar de acordo com a tecnologia utilizada, por isso não adicionaremos o mesmo ao nosso exemplo. A ideia é que tenhamos a possibilidade de escolher entre diversas tecnologias e então implementar essa interface que definimos agora para fazer com que ela tenha acesso aos dados reais. A Listagem 5 traz um exemplo de como ficaria um repositório concreto para a classe Produto, caso estivéssemos utilizando o Entity Framework. Aqui, vale ressaltar que o EF utiliza um DataContext para acesso aos dados, derivado da classe DbContext. Nesse caso, trata-se da classe DbDataContext. Outros ORM’s fazem uso de soluções similares, como o NHibernate com a interface ISession e o LINQ to SQL com a classe DataContext. É essa classe de contexto que contém os elementos da base de dados em si, normalmente da classe DbSet.

Listagem 5. Exemplo repositório concreto com EF

public class ProdutoRepository : IProdutoRepository
  {
      private DbDataContext _context = new DbDataContext();
   
      public ProdutoRepository(DbDataContext context)
      {
          _context = context;
      }
   
      public IEnumerable<Produto> Produtos
      {
          get { return _context.Produtos; }
      }
   
      public void Salvar(Produto prod)
      {
          if (prod.Id_Produto == 0)
          {
              _context.Produtos.Add(prod);
          }
          else
          {
              Produto dado = _context.Produtos.Find(prod.Id_Produto);
              if (dado != null)
              {
                  dado.Nome = prod.Nome;
                  dado.Descricao = prod.Descricao;
                  dado.Preco = prod.Preco;
                  dado.Quantidade = prod.Quantidade;
                  dado.Categoria = prod.Categoria;
              }
          }
      }
      void Adicionar(Produto produto)
      {...}
      IEnumerable<Produto> ListarTodos()
      {...}
      Produto BuscarPor(int idProduto)
      {...}
  }

Quando utilizamos o Entity Framework, normalmente haveria alguma alteração nas classes de domínio. O EF utiliza o conceito de propriedades virtuais, representando aquelas que não existem na base de dados. Assim, a classe Produto possuiria uma propriedade virtual do tipo Categoria (representando a categoria à qual ele pertence) e a lista de Produtos da classe Categoria seria virtual.

Como foi possível vermos ao longo do artigo, Domain-Driven Design é uma coleção de padrões e princípios que permitem que o desenvolvedor faça o design de uma aplicação de lógica de negócios complexa com mais facilidade. Foi possível notarmos que a utilização do padrão Domain Model, em conjunto com o padrão Repository, é uma excelente opção para o desenvolvimento de aplicações com essa metodologia, fazendo com que o domínio real da aplicação seja mais facilmente entendido.

Podemos aproveitar esse momento para fazer uma breve comparação entre o DDD e outras duas metodologias de design muito utilizadas: o TDD – Test-Driven Development – e o BDD – Behaviour-Driven Design. O primeiro diz respeito a permitir que os testes controlem o sistema; na prática, significa escrever os testes, inicialmente, e só então escrever o código que irá passar naqueles testes. Já o BDD é considerado uma união entre o TDD e o DDD; ele foca no comportamento do sistema (behaviour), utilizando uma linguagem que beneficia tanto desenvolvedores como usuários. Logo, podemos notar que há diferenças entre as três metodologias, e a escolha de qual delas devemos utilizar depende muito do sistema sendo desenvolvido.