Artigo no estilo: Curso Artigo no estilo Mentoring
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 envolvidasPara 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 ...