Artigo no estilo: Curso Artigo no estilo Mentoring

Mentoring: A divisão do projeto de software em camadas é uma prática que vem sendo utilizada há vários anos e, em boa parte dos casos, atende às necessidades iniciais dos sistemas. No entanto, à medida que a aplicação cresce, a utilização apenas das camadas DAL (Data Access Layer) e BLL (Business Logic Layer) acaba por não suportar adequadamente a expansão das funcionalidades, levando à sobrecarga de responsabilidades em alguns componentes. O Domain-Driven Design, apesar de ter sido proposto há mais de uma década, vem ganhando bastante atenção da comunidade técnica nos últimos anos e sendo usado tanto em novos projetos, quanto na refatoração de aplicações legadas. A sua utilização, juntamente com a correta aplicação dos principais conceitos de orientação a objeto e padrões de projeto, garante à aplicação uma arquitetura com baixo acoplamento e adequada divisão de responsabilidades entre os componentes.

Muitas vezes, por partirem de um conjunto de requisitos pequeno e sofrerem a comum pressão por entregas rápidas em prazos curtos, os desenvolvedores constroem seus sistemas com pouco ou nenhum projeto. Geralmente são utilizadas soluções de arquitetura e tecnologia simples que apenas atendem às necessidades daquele momento. Porém, naturalmente esses sistemas crescem e novas funcionalidades precisam ser implantadas, enquanto o tempo para atender às novas solicitações dos clientes continua resumido. Apesar de consciente da necessidade de refatoração no projeto, a equipe (principalmente quando é pequena e com pouca experiência) acaba por postergar essa tarefa, em alguns casos, até que ela seja imprescindível para a continuação da vida do software.

Aplicações legadas, construídas com tecnologias e arquiteturas pouco flexíveis, tendem a representar dificuldades para a equipe de desenvolvimento quando grandes manutenções são necessárias. Em certos casos é importante que a arquitetura da aplicação seja refatorada, bem como as tecnologias utilizadas sejam alteradas ou atualizadas para atender a novos requisitos do negócio.

Neste artigo veremos como realizar o refactoring completo de uma aplicação desenvolvida com a estrutura em três camadas mais comum em projetos .NET para uma arquitetura mais escalável através do DDD (Domain-Driven Design). Posteriormente o leitor poderá utilizar essa solução como template para migrar seus projetos para o DDD, inclusive visando outras arquiteturas evolutivas como microservices, e disponibilizar seus módulos em forma de serviços consumíveis.

Como cenário temos uma aplicação web de gerenciamento de tarefas desenvolvida com ASP.NET Web Forms, Entity Framework Code First e as seguintes camadas:

  • DAL (Data Access Layer), que contém as entidades e a persistência, com acesso direto à base de dados;
  • BLL (Business Logic Layer), a camada de negócios na qual colocamos todos os requisitos (regras de negócio) do sistema esperados pelos usuários;
  • Camada de apresentação contendo o ASP.NET Web Forms. Nessa parte, no entanto, foi utilizado o Bootstrap para a construção de uma interface de usuário agradável e responsiva, o que fará com que a maior parte do nosso trabalho seja direcionada às camadas inferiores.

Utilizaremos ainda o Entity Framework Fluent API, padrões SOLID, DRY, mantendo assim algumas das melhores práticas de programação. Já na camada de apresentação vamos utilizar o ASP.NET MVC 5 com AngularJS, tudo alinhado ao que prega o DDD, com foco no domínio da aplicação.

Sistema legado: Diagrama e camadas

O sistema de gerenciamento de tarefas é legado e foi desenvolvido através de um projeto de refactoring de uma aplicação desktop diretamente para a web. O sistema estava funcionando perfeitamente e de forma estável até que os clientes começaram a solicitar alterações pertinentes, mas a arquitetura que havia não comportava tamanha evolução. Na Figura 1 temos o diagrama arquitetônico da aplicação.

Essa figura demonstra bem como ficam distribuídas as camadas no projeto e como é o fluxo de dependências entre estas. Nossa tarefa é refatorar para uma nova arquitetura, mais escalável e moderna.

Figura 1. Diagrama arquitetônico do sistema legado

Fazendo um levantamento de como o sistema poderia ser evoluído, chegamos ao que vemos na Figura 2, um diagrama comparativo de como ficará o sistema e como redistribuiremos as regras de negócios existentes.

Figura 2. Diagramas de camadas conceituais

Nessa figura a diferença entre os modelos pode ser discreta e pode parecer que pouco irá mudar na arquitetura. Porém, será de grande valia para a manutenibilidade do sistema, pois a organização do projeto e do código que o DDD nos sugere fará toda a diferença.

Solução e ferramentas envolvidas

Para desenvolver o novo sistema utilizaremos o Visual Studio 2013, o .NET Framework 4.5, banco de dados SQL Server 2008 R2 Express, Entity Framework 6 (pacote EntityFramework), ASP.NET MVC 5 e o AngularJS 1.4.7 (pacote AngularJS). Esses últimos podem ser baixados via Nuget, bastando pesquisar pelos seus respectivos nomes e instalar os primeiros pacotes resultantes da busca.

Os códigos fontes completos, tanto do sistema legado quanto do novo, estão disponíveis para download, separados por solution, para que o leitor possa acompanhar melhor o artigo. Para manter o artigo conciso mostraremos aqui apenas os pontos principais e apontaremos onde fica cada novo trecho comparado com o equivalente no sistema legado.

Modelagem da nova estrutura

É importante que saibamos de onde partiremos e onde chegaremos a partir de cada elemento do projeto atual, para isso, veremos nas próximas seções o comparativo do código do sistema legado com o novo que será criado. Inicialmente podemos ver no diagrama da Figura 3 em qual local da nova arquitetura serão realocadas as antigas camadas utilizadas na arquitetura antiga.

Figura 3. Organização do código legado no novo projeto

Para facilitar e agilizar o processo de refatoração, trabalharemos para reaproveitar boa parte do código existente e reorganizá-lo nas camadas do novo sistema, agora de forma aderente ao que sugere o DDD.

Camada DAL: Entidades

No projeto original as entidades eram dispostas na camada DAL e estavam mapeadas através de anotações do Entity Framework Code First nas classes e propriedades, como vemos na Listagem 1. No novo projeto, as entidades ficam na camada de Domínio, porção central do projeto que segue o Doman Driven Design.

Listagem 1. Entidades no projeto legado


  01 [Table("TB_USUARIO")]
  02 public class Usuario
  03 {
  04     [Key]
  05     [Column("cod_usuario")]
  06     [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
  07     public virtual int Id { get; set; }
  08 
  09     [Column("nomecompleto")]
  10     [MaxLength(100)]
  11     [Required(AllowEmptyStrings = true, ErrorMessage = "É necessário que se entre com o nome completo.")]
  12     public virtual String NomeCompleto { get; set; }
  13      
  14      //...demais propriedades
  15 }
  16 
  17 [Table("TB_TAREFA")]
  18 public class Tarefa
  19 {
  20     [Key]
  21     [Column("cod_tarefa")]
  22     [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
  23     public virtual int Id { get; set; }
  24 
  25     [Column("nome")]
  26     [Required(AllowEmptyStrings = false, ErrorMessage = "É necessário que a tarefa tenha um nome.")]
  27     [MinLength(6, ErrorMessage = "Minimo de 6 caracteres para o nome.")]
  28     [MaxLength(100, ErrorMessage = "Maximo de 100 caracteres para o nome.")]
  29     public virtual String Nome { get; set; }
  30 
  31     [Column("dataEntrega")]
  32     [Required(AllowEmptyStrings = false, ErrorMessage = "É necessário que a tarefa contenha uma data de entrega.")]
  33     public virtual DateTime DataDeEntrega { get; set; }
  34
  35     //...demais propriedades
  36 }

Para simplificar a listagem omitimos algumas propriedades das duas entidades, mas é possível observar que temos as classes e propriedades decoradas com atributes, alguns contendo regras de validação. No novo projeto as regras de validação ficarão na camada de apresentação, dentro da pasta Models do projeto ASP.NET MVC, enquanto que os atributes referentes à configuração do Entity Framework serão colocados na camada de infraestrutura.

Na Listagem 2 vemos como essas mesmas entidades estarão na nova solução, mais limpas e respeitando o princípio da responsabilidade única. Além disso, elas agora serão também Persistent Ignorants, ou seja, desconhecem detalhes inerentes à persistência das informações no banco de dados. Dessa forma, essas classes não dependem do mecanismo de acesso ao banco de dados, estando livres para serem persistidas e recuperadas por qualquer framework/biblioteca. Desta forma, a escolha por qual utilizar (ADO.NET, Entity Framework, NHibernate) não interferirá naquilo que tem maior valor no DDD: o domínio da aplicação.

Listagem 2. Entidades refatoradas


  01 [Serializable]
  02 public class Tarefa
  03 {
  04     private Nullable<long> id;
  05     private string nome;
  06     private Nullable<DateTime> dataDaEntrega;
  07     private string descricao;
  08     private EstadoTarefa estado;
  09     private Nullable<long> idUsuario;
  10     private Usuario usuario;
  11 
  12     //...propriedades e métodos sobrescritos
  13 }
  14 
  15 [Serializable]
  16 public class Usuario
  17 {
  18     private Nullable<long> id;
  29     private string nomeCompleto;
  20     private string login;
  21     private string senha;
  22     private Status status;
  23     private ICollection<Tarefa> tarefas;
  24 
  25     //...propriedades e métodos sobrescritos
  26 }

Na Figura 4 vemos a posição das entidades dentro da camada de domínio do novo sistema, que foi construída como uma biblioteca de classes, apta a ser reaproveitada em outros projetos.

Figura 4. Entidades na camada de domínio na nova arquitetura

A configuração das entidades para o acesso do Entity Framework, por sua vez, foi colocada na camada de infraestrutura (também uma Class Library), em uma pasta específica para o ORM. A Figura 5 mostra como ficará, ao fim, essa camada.

Figura 5. Camada de infraestrutura

O mapeamento com a Fluent API utiliza convenções de nomenclatura e métodos que a partir de expressões lambda podem definir diversas características da entidade quando ela for persistida no banco de dados. Entre essas propriedades estão o nome da coluna (HasColumnName), comprimento (HasMaxLength) e obrigatoriedade do preenchimento (IsRequired). As classes responsáveis por realizar esse mapeamento podem ser vistas na Listagem 3.

Listagem 3. Configuração


  01 public class UsuarioMapeamento : EntityTypeConfiguration<Usuario>
  02 {
  03     public UsuarioMapeamento()
  04     {
  05         ToTable("TB_USUARIO");
  06 
  07         HasKey(u => u.Id);
  08 
  09         Property(u => u.Id).HasColumnName("cod_usuario");
  10         Property(u => u.Login).HasColumnName("login").HasMaxLength(20).IsRequired();
  11         Property(u => u.NomeCompleto).HasColumnName("nomecompleto").HasMaxLength(100).IsRequired();
  12         Property(u => u.Senha).HasColumnName("senha").HasMaxLength(500).IsRequired();
  13         Property(u => u.Status).HasColumnName("estado");            
  14 
  15     }
  16 }
  17
  18 public class TarefaMapeamento : EntityTypeConfiguration<Tarefa>
  19 {
  20     public TarefaMapeamento()
  21     {
  22         ToTable("TB_TAREFA");
  23 
  24         HasKey(t => t.Id);
  25         Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnName("cod_tarefa");
  26         Property(u => u.IdUsuario).HasColumnName("cod_usuario");
  27         Property(t => t.Nome).HasColumnName("nome").HasMaxLength(100).IsRequired();
  28         Property(t => t.DataDaEntrega).HasColumnName("dataentrega").IsRequired();
  29         Property(t => t.Descricao).HasColumnName("descricao").HasMaxLength(100);
  30         Property(t => t.Estado).HasColumnName("estado");
  31 
  32         HasRequired(t => t.Usuario).WithMany(u => u.Tarefas).HasForeignKey(t => t.IdUsuario);
  33     }
  34 }

A migração das entidades apenas não possui grande complexidade, a parte que requer maior trabalho são as classes de persistência, pois se comparado ao projeto original, essa tarefa será fragmentada na nova solução para garantir a flexibilidade do projeto.

Camada DAL: Persistência

Nessa camada mudaremos o paradigma que fora implementado até então, devido ao objeto de acesso a dados funcionar de maneira diferente de um objeto de infraestrutura. Então veremos inicialmente como está organizada a classe DAO (Data Access Object) de tarefas (Listagem 4) e como seu equivalente foi implementado na nova arquitetura. O mesmo é válido para os usuários, que segue a mesma lógica e sintaxe e, portanto, será omitido aqui para fins de simplificação do artigo.

Listagem 4. Classe TarefaDao


  01 public class TarefaDao : IDisposable
  02 {
  03     private ConexaoDeDados conexao;
  04 
  05     public void Criar(Tarefa tarefa)
  06     {
  07         try
  08         {
  09             using (ConexaoDeDados conexao = new ConexaoDeDados())
  10            {
  11                 conexao.TbTarefa.Add(tarefa);
  12                 conexao.SaveChanges();
  13             }
  14         }
  15         catch { throw; }
  16     }
  17 
  18     public void Editar(Tarefa tarefa)
  19     {
  20         try
  21         {
  22             using (ConexaoDeDados conexao = new ConexaoDeDados())
  23             {
  24                 conexao.Entry(tarefa).State = EntityState.Modified;
  25                 conexao.SaveChanges();
  26             }
  27         }
  28         catch { throw; }
  29     }
  30 
  31     public void Excluir(int id)
  32     {
  33         try
  34         {
  35             using (ConexaoDeDados conexao = new ConexaoDeDados())
  36             {
  37                 Tarefa tarefa = new Tarefa();
  38                 tarefa.Id = id;
  39                 conexao.TbTarefa.Remove(tarefa);
  40                 conexao.SaveChanges();
  41             }
  42         }
  43         catch { throw; }
  44     }
  45 
  46     public Tarefa BuscarPorId(int id)
  47     {
  48         try
  49         {
  50             using (ConexaoDeDados conexao = new ConexaoDeDados())
  51             {
  52                 var tarefaPorId = (from t in conexao.TbTarefa.AsNoTracking()
  53                                    where t.Id == id
  54                                    select t).FirstOrDefault<Tarefa>();
  55                 if (tarefaPorId != null && tarefaPorId.Id > 0)
  56                     return tarefaPorId;
  57                 else
  58                     return null;
  59             }
  60         }
  61         catch { throw; }
  62     }
  63 
  64     public ICollection<Tarefa> BuscarTodos()
  65     {
  66         try
  67         {
  68             using (conexao = new ConexaoDeDados())
  69             {
  70                 var tarefas = conexao.TbTarefa.AsNoTracking().ToList();
  71                 if (tarefas != null && tarefas.Count > 0)
  72                     return tarefas;
  73                 else
  74                     return null;
  75             }
  76         }
  77         catch { throw; }
  78     }
  79 }

No sistema legado essa classe é responsável por lidar com uma entidade para persisti-la e recuperá-la da base de dados, o que é bastante prático e adequado à arquitetura atual da aplicação. No entanto, isso não se aplica ao DDD, onde a persistência tem uma importância secundária e é feita através de contratos entre a camada de domínio e a camada de infraestrutura. Nesta segunda é onde são codificadas as operações de inserção, exclusão, alteração e busca propriamente ditas.

Para que possamos evoluir os DAOs do projeto atual, precisamos adequá-los à forma de desenvolvimento dirigido por interfaces, ou fachadas, de forma a segmentar e permitir o acesso apenas ao que for permitido por esses componentes.

O primeiro procedimento para isso é extrair os métodos da persistência dessas classes e realoca-los em interfaces na camada de domínio, às quais também chamamos de contratos. Além disso, também temos de converter as regras existentes na classe para adequá-las ao DDD. Por exemplo, colocaremos o domínio real como parâmetro e não apenas trechos como Id, nome e outros parâmetros resumidos.

Como sabemos que as ações se repetem entre os DAOs, podemos criar uma interface principal genérica e dela derivar as interfaces especializadas para tratar das tarefas e usuários que, se necessário, podem receber novas ações específicas

Na Listagem 5 temos a interface IRepositorioPai, que recebe como argumento um tipo genérico T a ser persistido. Logo abaixo temos a interface ITarefaRepositorio que já usa o tipo concreto Tarefa e por não ter nenhum comportamento adicional, não precisa definir nenhum novo método ou propriedade, aproveitando as definições da interface anterior. O mesmo vale para a interface IUsuarioRepositorio.

Listagem 5. Interfaces da camada de domínio


  01 public interface IRepositorioPai<T> : IUnitOfWork
  02     where T : class
  03 {
  04     void Criar(T entidade);
  05     void Editar(T entidade);
  06     void Excluir(T entidade);
  07     ICollection<T> BuscarTodos(); 
  08     ICollection<T> Fi ... 

Quer ler esse conteúdo completo? Tenha acesso completo