Junho de 2019

Volume 34 – Número 6

[Padrões e Práticas]

Desenvolvimento super DRY para ASP.NET Core

Por Thomas Hansen

DRY é um desses acrônimos de arquitetura de software realmente importantes. Significa "Don’t Repeat Yourself" (Não se repita) e articula um princípio fundamental para qualquer pessoa que tenha mantido um projeto de código-fonte herdado. Ou seja, se você se repetir no código, você descobrirá que cada correção de bug e atualização de recurso fará com que você repita suas modificações.

A repetição de código reduz a capacidade de manutenção do seu projeto e torna mais difícil aplicar as alterações. E quanto mais repetição você tem, mais código espaguete você terá. Se, por outro lado, você evitar a repetição, você pode acabar com um projeto que é significativamente mais fácil de manter e de ter seus bugs corrigidos. Você será um desenvolvedor de software mais feliz e produtivo. Em resumo, o código DRY pode ajudar a criar um código excelente.

Depois de começar a pensar com a mentalidade do DRY, você poderá usar esse importante princípio de arquitetura de maneira nova, como se o seu projeto fosse criado em um passe de mágica — literalmente, sem a necessidade de aplicar qualquer esforço para criar funcionalidade. Para os não iniciados, pode parecer que os mecanismos do “super-DRY” fazem com que o código surja do nada. Um ótimo código quase sempre é pequeno, mas códigos excelentes são ainda menores.

Neste artigo, apresentarei a vocês a mágica do desenvolvimento super-DRY, além de alguns dos truques que usei ao longo dos anos que podem ajudá-los a criar as APIs Web do ASP.NET Core com muito menos esforço. Tudo neste artigo se baseia em soluções generalizadas e o conceito de código DRY, usando apenas as melhores práticas do nosso setor. Mas, primeiramente, algumas informações básicas.

CRUD, REST HTTP e SQL

O CRUD (Criar, ler, atualizar e excluir) é o comportamento básico da maioria dos modelos de dados. Na maioria dos casos, seus tipos de entidade de dados precisam dessas quatro operações e, na verdade, HTTP e SQL são, indiscutivelmente, criados ao redor delas. HTTP POST é para a criação de itens, HTTP GET é para a leitura de itens, HTTP PUT serve para atualizar seus itens e HTTP DELETE destina-se à exclusão de itens. Da mesma forma, SQL evolui em torno do CRUD com inserção, seleção, atualização e exclusão. Depois que você o analisa um pouco, fica bastante óbvio que, basicamente, tudo tem a ver com CRUD, supondo que você não deseja “se comprometer totalmente” e implementar uma arquitetura CQRS.

Você tem os mecanismos de linguagem necessários para falar sobre verbos HTTP, de forma que eles se propaguem a partir da camada HTTP do cliente, por meio de seu código C#, até seu banco de dados relacional. Agora, tudo o que você precisa é uma forma genérica de implementar essas ideias por meio de suas camadas. E é importante que você faça isso sem se repetir, com uma base de arquitetura brilhante. Vamos começar.

Primeiramente, baixe o código em github.com/polterguy/magic/releases. Descompacte o arquivo e abra magic.sln no Visual Studio. Inicie o depurador e observe como você já terá cinco pontos de extremidade REST HTTP na interface do usuário do Swagger. De onde vieram esses pontos de extremidade HTTP? Bem, vamos analisar o código, pois a resposta para essa pergunta pode surpreender você.

Vejam só, nenhum código!

A primeira coisa que você observará conforme começa a procurar o código é que o projeto Web do ASP.NET Core em si é literalmente vazio. Isso é possível devido a um recurso do ASP.NET Core que permite que você inclua controladores de maneira dinâmica. Se você quiser ver os recursos internos por trás disso, confira o arquivo Startup.cs. Basicamente, ele está adicionando dinamicamente cada controlador de todos os assemblies na sua pasta do AppDomain. Essa ideia simples permite reutilizar seus controladores e pensar de forma modularizada conforme cria suas soluções. A capacidade de reutilizar controladores em vários projetos é a etapa número 1 no caminho certo para se tornar um profissional de super-DRY.

Abra o projeto web/controller/magic.todo.web.controller e analise o arquivo TodoController.cs. Você observará que ele está vazio. Portanto, de onde vieram esses cinco pontos de extremidade REST HTTP? A resposta é: por meio do mecanismo de programação orientada a objeto (OOP) e C# genéricos. A classe TodoController é herdada do CrudController, passando pelo modelo de exibição e seu modelo de banco de dados. Além disso, está usando a injeção de dependência para criar uma instância do ITodoService, que é transferida para a classe base CrudController.

Como a interface ITodoService herda do ICrudService com a classe genérica correta, a classe base CrudController aceita a sua instância de serviço. Além disso, neste ponto, ela pode já usar o serviço “polimorfisticamente”, como se fosse uma simples ICrudService, que, logicamente, é uma interface genérica com tipos parametrizados. Isso fornece acesso aos cinco métodos de serviço genericamente definidos no CrudController. Para entender as implicações disso, perceba que, com o seguinte código simples, você criou, literalmente, todas as operações de CRUD necessárias. Você as propagou da camada de REST HTTP, por meio da camada de serviço, para a hierarquia de classes de domínio, terminando na camada de banco de dados relacional. Aqui está todo o código para seu ponto de extremidade do controlador:

[Route("api/todo")]
public class TodoController : CrudController<www.Todo, db.Todo>
{
  public TodoController(ITodoService service)
    : base(service)
  { }
}

Esse código fornece cinco pontos de extremidade REST HTTP, permitindo criar, ler, atualizar, excluir e contar itens de banco de dados, quase como em um passe de mágica. E seu código inteiro foi "declarado" e não continha uma única linha de funcionalidade. Agora, é claro que os códigos não são criados sozinhos. Grande parte do trabalho é feito em segundo plano, mas o código aqui se tornou “Super-DRY”. Há uma vantagem real de se trabalhar com um nível mais alto de abstração. Uma boa analogia seria a relação entre uma instrução C# de simulação e o código da linguagem assembly subjacente. A abordagem que descrevi é simplesmente um nível mais alto de abstração do que codificar seu código de controlador.

Nesse caso, o tipo www.Code é seu modelo de exibição, o tipo db.Todo é o seu modelo de banco de dados e ITodoService é sua implementação de serviço. Ao simplesmente sugerir à classe base que tipo você deseja persistir, seu trabalho terminou. Novamente, a camada de serviço também fica vazia. Seu código inteiro pode ser visto aqui:

public class TodoService : CrudService<Todo>, ITodoService
{
  public TodoService([Named("default")] ISession session)
    : base(session, LogManager.GetLogger(typeof(TodoService)))
  { }
}

Nenhum método, nenhuma propriedade e nenhum campo, e ainda há uma camada de serviço completa para seus itens TODO. Na verdade, até mesmo a interface de serviço está vazia. O exemplo a seguir mostra todo o código para a interface de serviço:

public interface ITodoService : ICrudService<Todo>
{ }

Novamente, vazio! Ainda assim — sim salabim, abracadabra — e você tem um aplicativo de API Web REST HTTP TODO. Se você abrir o modelo de banco de dados, você verá o seguinte:

public class Todo : Model
{
  public virtual string Header { get; set; }
  public virtual string Description { get; set; }
  public virtual bool Done { get; set; }
}

Mais uma vez, não há nada aqui — apenas algumas propriedades virtuais e uma classe base. E ainda assim, você é capaz de persistir o tipo em seu banco de dados. O mapeamento real entre seu banco de dados e seu tipo de domínio ocorre na classe TodoMap.cs, dentro do projeto magic.todo.model. Aqui, você pode ver a classe inteira:

public class TodoMap : ClassMap<Todo>
{
  public TodoMap()
  {
    Table("todos");
    Id(x => x.Id);
    Map(x => x.Header).Not.Nullable().Length(256);
    Map(x => x.Description).Not.Nullable().Length(4096);
    Map(x => x.Done).Not.Nullable();
  }
}

Este código instrui a biblioteca ORM para usar a tabela todos, com a propriedade Id como chave primária e define algumas propriedades adicionais para o restante das colunas/propriedades. Observe que quando você iniciou esse projeto, você ainda nem tinha um banco de dados. Isso ocorre porque o NHibernate cria automaticamente as tabelas de banco de dados se elas ainda não existirem. E como o Magic, por padrão, está usando SQLite, você nem precisa de uma cadeia de conexão. Ele criará automaticamente um banco de dados SQLite baseado em arquivo em um caminho de arquivo relativo, a menos que você substitua suas configurações de conexão no appsettings.config para usar MySQL ou MSSQL.

Acredite ou não, sua solução já oferece suporte, de maneira transparente, a quase todos os bancos de dados relacionais que você pode imaginar. Na verdade, a única linha de código real que você precisaria adicionar para fazer isso funcionar pode ser encontrada no projeto magic.todo.services, dentro da classe ConfigureNinject, que simplesmente associa a interface de serviço e a implementação do serviço. Então, sem dúvida, você adicionou uma linha de código e tem um aplicativo inteiro como o resultado disso. A seguir, veja a única linha de “código” real usada para criar o aplicativo TODO:

public class ConfigureNinject : IConfigureNinject
{
  public void Configure(IKernel kernel, Configuration configuration)
  {
    // Warning, this is a line of C# code!
    kernel.Bind<ITodoService>().To<TodoService>();
  }
}

Podemos fazer mágica com o super-DRY, por meio do uso inteligente de OOP, polimorfismo paramétrico e os princípios de DRY. Então, a pergunta é: Como você pode usar essa abordagem para melhorar seu código?

A resposta: Comece com o seu modelo de banco de dados e crie sua própria classe de modelo. Isso pode ser feito adicionando um novo projeto dentro de sua pasta de modelo, ou adicionando uma nova classe ao projeto magic.todo.model existente. Em seguida, crie sua interface de serviço na pasta de contratos. Agora, implemente seu serviço na pasta de serviços e crie seu modelo e controlador de exibição. Certifique-se de associar sua interface de serviço e sua implementação de serviço. Em seguida, se você optar por criar novos projetos, você terá que garantir que o ASP.NET Core carregue seu assembly adicionando uma referência a ele em seu projeto magic.backend. Observe que apenas o projeto de serviço e o controlador precisam ser referenciados por seu back-end.

Se você optar por usar os projetos existentes, a última parte nem sequer é necessária. Basta uma simples linha de código real para associar sua implementação de serviço e sua interface de serviço e você terá criado uma solução inteira de API Web ASP.NET Core. Você deve estar imaginando que essa linha é realmente complexa. Você pode me acompanhar em todo o processo e conferir uma versão prévia do código. Assista ao vídeo “Super DRY Magic for ASP.NET Core” (A mágica do Super DRY para ASP.NET Core) disponível em youtu.be/M3uKdPAvS1I.

Em seguida, imagine o que ocorre quando você percebe que pode desenvolver esse código e gerá-lo automaticamente de acordo com o esquema do banco de dados. Neste ponto, o sistema de software scaffolding do seu computador está, sem dúvida alguma, fazendo a codificação, produzindo uma arquitetura DDD (Domain-Driven Design) perfeitamente válida no processo.

Sem código, sem bugs, sem problemas

A maioria das estruturas scaffolding aplicam atalhos ou impedem que você estenda e modifique o código resultante, de modo que usá-los para aplicativos do mundo real se torna algo impossível. Com o Magic, essa deficiência não acontece. Ele cria uma camada de serviço para você e usa a injeção de dependência para injetar uma interface de serviço ao seu controlador. Ela também produz padrões DDD perfeitamente válidos. E como você criou seu código inicial, todas as partes da sua solução podem ser estendidas e modificadas conforme necessário. Seu projeto é perfeitamente válido para cada letra do SOLID.

Por exemplo, em uma das minhas próprias soluções, tenho um thread fetcher de servidor POP3 em meu serviço, declarado para um tipo de modelo de domínio EmailAccount. Esse serviço POP3 armazena emails no meu banco de dados do servidor POP3, executado em um thread de segundo plano. Quando um email é excluído, eu quero garantir que seus anexos também sejam fisicamente excluídos do armazenamento, e se o usuário exclui um EmailAccount, obviamente, quero excluir seus emails associados.

O código na Figura 1 mostra como eu substituí a exclusão de um EmailAccount, que também deve excluir todos os emails e anexos. Só para constar: ele usa a Hibernate Query Language (HQL) para se comunicar com o banco de dados. Isso garante que o NHibernate crie automaticamente a sintaxe SQL correta, dependendo do banco de dados ao qual ele está fisicamente conectado.

Figura 1 Como substituir a exclusão de um EmailAccount

public sealed class EmailAccountService : CrudService<EmailAccount>,
  IEmailAccountService
{
  public EmailAccountService(ISession session)
    : base(session, LogManager.GetLogger(typeof(EmailAccountService)))
  { }
  public override void Delete(Guid id)
  {
    var attachments = Session.CreateQuery(
      "select Path from EmailAttachment where Email.EmailAccount.Id = :id");
    attachments.SetParameter("id", id);
    foreach (var idx in attachments.Enumerable<string>())
    {
      if (File.Exists(idx))
        File.Delete(idx);
    }
    var deleteEmails = Session.CreateQuery(
      "delete from Email where EmailAccount.Id = :id");
    deleteEmails.SetParameter("id", id);
    deleteEmails.ExecuteUpdate();
    base.Delete(id);
  }
}

Fazendo as contas

A inspiração surge assim que você começa a pensar sobre essas ideias. Por exemplo, imagine uma estrutura de scaffolding criada em torno do Magic. Do ponto de vista matemático, se você tiver um banco de dados com 100 tabelas, cada uma com uma média de 10 colunas, verá que o custo em termos de linhas totais de código pode aumentar rapidamente. Por exemplo, para envolver todas essas tabelas em uma API REST HTTP, são necessárias sete linhas de código por interface de serviço, enquanto 14 linhas de código são necessárias por tabela para cada serviço e 19 linhas são necessárias por tabela para cada controlador. Figura 2 apresenta os elementos envolvidos e as linhas de código necessárias.

Figura 2 Como adicionar o custo ao código

Componente Contratos Média de linhas de código Total de linhas de código
Interfaces de serviço 100 7 700
Serviços 100 14 1.400
Controladores 100 19 1.900
Implementação e interface de serviço 100 1 100
Modelos de exibição 100 17 1.700
Modelos de banco de dados 100 17 1.700
Mapeamentos de banco de dados 100 20 2.000
Total de linhas de código: 9.500

 

Depois de definir tudo, você estará olhando para 9.500 linhas de código. Se você construir um serviço meta capaz de extrair um esquema de banco de dados existente, ficará bastante óbvio que você pode gerar esse código usando scaffolding — evitando qualquer codificação, e ainda produzindo 9.500 linhas de código perfeitamente arquitetado, facilmente estendidas, usando todos os padrões de design relevantes e melhores práticas. Com apenas dois segundos de scaffolding, seu computador fez 80% do seu trabalho.

Tudo que você precisa fazer agora é analisar os resultados do processo de scaffolding e substituir métodos para seus serviços e controladores para os tipos de domínio que exigem atenção especial por algum motivo. Sua API Web está pronta. Como todos os pontos de extremidade do controlador têm exatamente a mesma estrutura, duplicar esse processo de scaffolding na camada do cliente é tão fácil quanto ler os arquivos de declaração JSON da API gerados pelo Swagger. Isso permite que você crie sua camada de serviço para algo como Angular ou React. E tudo isso porque seu código e sua API Web possuem estruturas previsíveis, baseadas em princípios de generalização e na prevenção da repetição.

Para colocar isso em perspectiva, você conseguiu criar um projeto de API Web REST HTTP que provavelmente tem o dobro do tamanho do projeto de código aberto Sugar CRM em termos de complexidade, e você fez 80% do trabalho em segundos. Você facilitou uma linha de montagem de fábrica de software com base na padronização de componentes e na reutilização de estrutura, além de facilitar a leitura e a manutenção do código de todos os seus projetos. Mesmo as partes que exigem modificação e comportamento especial podem ser reutilizadas em seu próximo projeto, graças à forma como os pontos de extremidade e os serviços do controlador são carregados dinamicamente em sua API Web, sem nenhuma dependência.

Se você trabalha para uma empresa de consultoria, provavelmente inicia vários novos projetos a cada ano com tipos semelhantes de requisitos, nos quais precisa resolver os pontos em comum para cada novo projeto. Com uma compreensão dos requisitos de um cliente e alguma implementação inicial, uma abordagem super-DRY permite que você literalmente termine um projeto inteiro em segundos. E, claro, a composição dos elementos em seus projetos pode ser reutilizada, identificando módulos comuns, como Autenticação e Autorização. Ao implementar esses módulos em um projeto de API Web comum, você pode aplicá-los a qualquer novo projeto que apresente problemas semelhantes aos que você já viu antes.

Só para constar: falar é fácil, mas na realidade, evitar a repetição é algo difícil. Isso requer disposição para refatorar, refatorar e refatorar. E quando você terminar de refatorar, precisará refatorar um pouco mais. Mas a vantagem é boa demais para ser ignorada. Os princípios DRY podem permitir que você crie código de forma quase mágica, simplesmente usando seus recursos de scaffolding e compondo módulos com base em peças pré-existentes.

No final das contas, os princípios aqui descritos podem ajudar você a aproveitar as melhores práticas existentes para criar suas próprias APIs Web, evitando a repetição. Há muita coisa boa que pode vir dessa abordagem e espero que ela ajude você a apreciar as qualidades do DRY.


Atualmente, Thomas Hansen trabalha como assistente de software Zen no Chipre, manipulando códigos de software para a FinTech e sistemas comerciais.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: James McCaffrey


Discuta esse artigo no fórum do MSDN Magazine