.NET -  Onion Architecture : Criando um projeto fácil de manter


 Neste artigo vou apresentar os principais conceitos da Onion Architecture e como podemos usá-los para criar um projeto de software que seja fácil de manter, testar e estender.


O termo Onion Architecture foi cunhado por Jeffrey Palermo em 2008. Essa arquitetura fornece uma alternativa robusta para criar aplicativos para uma melhor testabilidade, manutenção e confiabilidade.

Criando um projeto de software sustentável

No desenvolvimento de software, uma das coisas mais importantes a se ter em mente é que o software deve estar sempre em evolução, recebendo  novas funcionalidades, melhorias e correções de bugs.

Portanto, podemos ver que é importante construir um software que seja sustentável, ou seja, um software que possa ser mantido hoje e no futuro independente de quem for trabalhar com ele.

Mas, o que é software sustentável ?

Um software sustentável é um software que qualquer desenvolvedor deve ser capaz de fazer melhorias e correções sem se preocupar em quebrar o código. Qualquer desenvolvedor, familiarizado com o domínio, deve ser capaz de entender o código e saber facilmente onde alterar as coisas.

Dessa forma modificar a camada de apresentação não deve quebrar nenhuma lógica de domínio, modificar a modelagem de banco de dados não deve afetar as regras de negócios do projeto. Além disso, a lógica do domínio  deve ser capaz de ser testada facilmente.

Com essa premissa, devemos começar a pensar em separar diferentes interesses em diferentes unidades de código usando uma arquitetura que nos auxilie neste objetivo.

O que é a Onion Architecture ?

A Onion Architecture é um padrão de arquitetura que propõe que o software deve ser feito em camadas, cada camada com sua própria preocupação ou responsabilidade, e, foi proposta por Jeffrey Pallermo em seu site.

Abaixo temos uma figura que mostra a disposição das camadas na Onion Architecture :



A regra de ouro da arquitetura é:  "Nada em um círculo interno pode saber absolutamente nada sobre algo em um círculo externo. Isso inclui métodos, classes, variáveis ou qualquer outra entidade de software nomeada."  Robert C. Martin

Obs:Essa regra também existe em outras arquiteturas semelhantes, como Arquitetura Limpa (Clean Architecture).

Esta regra depende da injeção de dependência para fazer sua abstração das camadas, para que você possa isolar suas regras de negócios de seu código de infraestrutura, como repositórios e views.

A Onion Architecture propõe três camadas diferentes:
  1. A Camada de Domínio (Domain Layer)
  2. A Camada de Aplicação (Application Layer)
  3. A Camada de infraestrutura (Infrastructure Layer)

A seguir vamos descrever essas camadas e os recursos que cada uma utiliza.

A camada de Domínio (Domain Layer)

A Camada de Domínio é a camada mais interna da arquitetura.

Os modelos de domínio e os modelos de serviços estarão dentro desta camada, contendo todas as regras de negócio do software que devem ser puramente lógicas, não realizando nenhuma operação IO. Como essa camada é puramente lógica, deve ser muito fácil testá-la, pois você não precisa se preocupar em simular operações de IO.

Ao isolar sua lógica de domínio, o domínio se torna fácil de testar e manter.

Essa camada também não pode saber sobre o que for declarado nas camadas Application ou Infrastructure.

Modelos de domínio

Os Modelos de Domínio são o núcleo da Camada de Domínio. Eles representam os modelos de negócios, contendo as regras de negócios de seu domínio.

Serviços de domínio

Existem alguns casos em que é difícil ajustar um comportamento em um único modelo de domínio. Imagine que você está modelando um sistema bancário, onde tem o modelo de domínio Conta. Em seguida, você precisa implementar o recurso de transferência, que envolve duas contas.

Não está tão claro se esse comportamento deve ser implementado pelo modelo de Conta, então você pode escolher implementá-lo em um serviço de domínio.

Assim, um serviço de domínio contém um comportamento que não está vinculado a um modelo de domínio específico. Pode ser basicamente uma função, em vez de um método. Observe que, você deve sempre tentar implementar comportamentos em Modelos de Domínio para evitar cair na armadilha do Modelo de Domínio Anêmico.

Camada de aplicação (Application Layer)

A Camada de Aplicação é a segunda camada mais interna da arquitetura.

Esta camada é responsável por preparar o ambiente para seus modelos, para que possam executar suas regras de negócio.

Esta camada implementa regras de aplicativo (às vezes chamadas de casos de uso) em vez de regras de negócios. As regras de aplicativos são diferentes das regras de negócios. As primeiras são regras que são executadas para implementar um caso de uso de seu aplicativo. Estas últimas são regras que pertencem ao próprio negócio.

Exemplo de regra de aplicativo: 

  • Carregue uma conta de um repositório, chame o método Conta.Sacar() passando uma quantia e envie o novo saldo no e-mail do proprietário da Conta;

Exemplo de regra de negócios:

  • Quando um Saque for solicitado em uma Conta, uma taxa de 0,1% deve ser cobrada do proprietário da conta;

Portanto, com base nos exemplos, se o sistema não tivesse ainda sido criado,  as regras de negócios ainda seriam aplicadas. As regras do aplicativo não, pois dependem do aplicativo.

Observe que a própria camada de aplicativo não implementa nenhuma operação IO. A única camada que implementa IO é a camada de infraestrutura.

A camada de aplicativo apenas chama métodos de objetos que implementam as interfaces que ela espera, e esses objetos (da camada de infraestrutura) podem fazer algum operação IO.

Serviços

Um serviço de aplicativo é um pedaço de código que implementa um caso de uso.

Ele pode receber objetos que implementam algumas interfaces conhecidas (injeção de dependência) e tem permissão para importar entidades da Camada de Domínio.

Interfaces

Se a camada de aplicativo deve coordenar operações que envolvem operações IO, como carregar dados de um repositório ou enviar um e-mail, ela deve declarar algumas interfaces com os métodos que deseja usar.

Esta camada não deve se preocupar em implementar esses métodos, apenas declarar suas assinaturas.

Data Transfer Objects - DTOs

Um Data Transfer Object (DTO) é um objeto que contém dados que serão transferidos entre camadas diferentes, em algum formato específico.
Às vezes, você deseja transferir dados que não são exatamente um Modelo de Domínio ou um Objeto de Valor.

Por exemplo, digamos que você esteja desenvolvendo um sistema bancário. Você está implementando um caso de uso que permite ao usuário verificar o saldo de sua conta.

Portanto, o caso de uso seria algo assim:

- O aplicativo recebe uma solicitação;
- A conta do usuário é carregada de um repositório;
- O aplicativo retorna um objeto contendo o saldo da conta, o carimbo de data/hora atual, o ID da solicitação e alguns outros dados secundários para fins de auditoria.

Ao final do processo será retornado um objeto contendo os dados obtidos nesta operação.

Este objeto não tem comportamento. Ele contém apenas dados e é usado apenas neste caso de uso como um valor de retorno. Neste cenário este objeto pode ser considerado DTO.

Os DTOs são adequados como objetos com formatos e dados realmente específicos. Normalmente, você não deve implementá-los querendo reutilizá-los em outros casos de uso, pois isso uniria os dois casos de uso diferentes.

Camada de infraestrutura

A camada de infraestrutura é a camada mais externa da arquitetura Onion. Ela é responsável por implementar todas as operações IO que são necessárias para o software.

Esta camada também pode saber tudo o que está contido nas camadas internas, podendo importar entidades das camadas de Aplicação e de Domínio. A camada de infraestrutura não deve implementar nenhuma lógica de negócios, bem como qualquer fluxo de caso de uso.

Os Repositórios, APIs externas, ouvintes de Eventos e todos os outros códigos que lidam com IO de alguma forma devem ser implementados nesta camada.

Repositórios

Um Repositório é um padrão para uma coleção de objetos de domínio.

Ele é responsável por lidar com a persistência (como um banco de dados) e atua como uma coleção de objetos de domínio na memória. Normalmente, cada agregado de domínio tem seu próprio repositório (se ele deve ser persistido), então você pode ter um repositório para contas, outro para clientes e assim por diante.

Normalmente não é uma boa ideia tentar usar um único repositório para mais de um agregado, porque talvez você acabe tendo um Repositório Genérico.

Na Onion Architecture, o banco de dados é apenas um detalhe da infraestrutura. O resto do seu código não deve se preocupar se você estiver armazenando seus dados em um banco de dados, em um arquivo ou apenas na memória.

Camada de apresentação - Views

As partes de seu código que expõem seu aplicativo para o mundo externo também fazem parte da camada de infraestrutura, pois lidam com IO.
As camadas internas não devem saber se seu aplicativo está sendo exposto por meio de uma API, por meio de uma CLI ou qualquer outra coisa.

Vantagens da Onion Architecture

A Onion Architecture, como qualquer padrão, tem suas vantagens e desvantagens.

Vejamos as vantagens:

1- De fácil manutenção

É mais fácil manter um aplicativo que tenha uma boa separação de interesses. Você pode alterar as coisas na camada de infraestrutura sem ter que se preocupar em quebrar uma regra de negócios.

É fácil descobrir onde estão as regras de negócios, os casos de uso, o código que lida com o banco de dados, o código que expõe uma API e assim por diante.

Além disso, o código é mais fácil de testar devido à injeção de dependência, o que também contribui para tornar o software mais sustentável.
Linguagem e estrutura independente

A Onion Architecture não depende de nenhuma linguagem ou estrutura específica. Você pode implementá-lo basicamente em qualquer linguagem que ofereça suporte à injeção de dependência.

2 - Injeção de dependência até o fim! Fácil de testar

Ao fazer injeção de dependência em todo o código, tudo se torna mais fácil de testar.

Em vez de cada módulo ser responsável por instanciar suas próprias dependências, ele tem suas dependências injetadas durante a inicialização. Dessa forma, quando você quiser testá-lo, pode apenas injetar um mock que implementa a interface que seu código está esperando.

Desvantagens

No entanto, como não existe a tal 'bala de prata', existem algumas desvantagens.

1- É complicado quando você não tem muitas regras de negócios

Quando você está criando um software que não lida com regras de negócios, essa arquitetura não se ajusta bem. Seria realmente complicado implementar, por exemplo, um gateway simples usando a Onion Architecture.

Essa arquitetura deve ser usada ao criar serviços que lidam com regras de negócios. Se não for esse o caso, só desperdiçará seu tempo.

2- Curva de aprendizado não tão fácil

Pode ser difícil implementar um serviço usando a Onion Architecture quando você tem um histórico centrado no banco de dados.

A mudança de paradigma não é tão direta, portanto, você precisará investir algum tempo no aprendizado da arquitetura antes de poder usá-la sem esforço.

Armadilhas para evitar

Existem algumas armadilhas que você deve evitar ao usar esta arquitetura.

Modelos de domínio anêmico

Quando todas as suas regras de negócios estão em serviços de domínio em vez de em seus modelos de domínio, provavelmente você tem um
Modelo de Domínio Anêmico.

Um modelo de domínio anêmico é um modelo de domínio que não tem comportamento, apenas dados. Ele atua como um saco de dados, enquanto o próprio comportamento é implementado em um serviço.

Este anti-padrão tem muitos problemas que são bem descritos no artigo de Fowler.

Observe que os Modelos de Domínio Anêmico são um anti-padrão ao trabalhar em linguagens OOP, porque, ao executar uma regra de negócios, espera-se que você altere o estado atual de um objeto de domínio.

Não comece modelando o banco de dados

Naturalmente, talvez você queira iniciar o desenvolvimento pelo banco de dados, mas é um erro! Ao trabalhar com a Onion Architecture, você deve sempre começar a desenvolver as camadas internas antes das externas.

Portanto, você deve começar modelando sua camada de domínio, em vez da camada de banco de dados. No Onion, o banco de dados é apenas um detalhe.

Seria muito difícil começar pelos repositórios, porque:

- Os repositórios dependem da Camada de Domínio, pois atuam como uma coleção de objetos de domínio.
- Eles também dependem das interfaces definidas pela Camada de Aplicativo, portanto, você ainda não sabe quais métodos precisará implementar.

Arquiteturas semelhantes

Existem outras arquiteturas semelhantes que usam alguns dos mesmos princípios.

- Arquitetura Limpa
- Arquitetura Hexagonal ou Portas e adaptadores
 
E estamos conversados...

"Melhor é o que tarda em irar-se do que o poderoso, e o que controla o seu ânimo do que aquele que toma uma cidade."
Provérbios 16:32

Referências:


José Carlos Macoratti