DEV Community

Cover image for Modelo de Desing de Aplicações Backend
Bruno Brolesi
Bruno Brolesi

Posted on

Modelo de Desing de Aplicações Backend

  1. Introdução
  2. Camadas
  3. Boas práticas
  4. Fluxo de desenvolvimento
  5. Ponto de atenção

Introdução

Após alguns anos trabalhando em aplicações utilizando diferentes modelos de desing e colhendo feedbacks positivos e negativos de como as implementações se comportavam ao longo do tempo, escrevi esse artigo propondo um modelo de design de código para o desenvolvimento de aplicações backend. Nele trago o que mais funcionou nas equipes que trabalhei e considero um modelo com um nível ideal entre abstrações e separação de responsabilidade. Esse modelo traz uma mistura de conceitos de Arquitetura Hexagonal, Clean Arch e DDD.

A imagem abaixo contém um overview do modelo que será destrinchado ao longo desse artigo.

Imagem contendo blocos que representam cada camada e subcamada da aplicação,cada camada será explicada ao longo do artigo

Antes de entrarmos afundo nos detalhes de cada módulo, vamos realizar uma breve jornada entre as camadas da nossa aplicação:

  1. Eu como cliente realizo uma chamada para a aplicação, ela será recebida na camada de delivery, onde todos os dados que foram inseridos na chamada serão extraídos e devidamente validados. Caso os dados sejam inválidos a requisição já retorna para o usuário nessa camada.

  2. Como os dados são válidos, a aplicação encaminha o tratamento da requisição para a camada de usecase, nela todas as regras de negócio que são necessárias para processar a requisição são aplicadas.

  3. Se o usecase necessitar de um serviço externo para processar a requisição, por exemplo um banco de dados, ele ira recorrer a camada de gateway, ela contém todas as interfaces que definem o contrato das implementações que se comunicam com serviços externos.

  4. Como o usecase esta visualizando apenas uma interface, é nesse momento que a real implementação do serviço externo é chamada. Depois desse passo, acontece o caminho reverso para devolver a resposta ao cliente, os dados são retornados para o usecase, onde ele pode ou não chamar outros serviços, o usecase devolve o resultado para a camada de delivery, sendo ele de sucesso ou falha e o resultado é devolvido ao cliente.

Camadas

Agora iremos entrar em cada componente das camadas da imagem para explicar as suas responsabilidades.

Ao analisarmos a parte mais externa desse modelo de design, podemos visualizar dois pacotes: infrastructure e business. Vamos nos aprofundar em cada um deles individualmente.

Infrastructure

infrastructure: esse pacote é responsável por conter tudo que não tem relação com o domínio da nossa aplicação, ou seja, tudo aquilo que não faz parte da nossa regra de negócio, protocolos de comunicação, framework , conexão com banco de dados, etc. A seguir vamos entender quais são os pacotes internos do módulo de infrastructure.

  • delivery: esse pacote é responsável por conter tudo aquilo que é referente a comunicação com o client utilizador da nossa aplicação, seja ele um webapp, cli, etc. Este pacote contém sub pacotes para facilitar a organização de cada responsabilidade.

    • webapp: esse pacote é responsável por conter nosso web server, as rotas que a aplicação expõe e tudo aquilo relacionado com o tratamento de requisições http para nossa aplicação. Dentro dele existem outros sub pacotes para facilitar a organização de cada responsabilidade.
    • middlewares: esse pacote é responsável por conter os middlewares da aplicação.
    • handlers: esse pacote é responsável por conter os handlers http da nossa aplicação. Nessa etapa todos os dados necessário da request são extraídos e validados, posteriormente sendo utilizados na chamada dos use cases.
    • consumers: esse pacote é responsável por conter os handlers referentes aos consumers da nossa aplicação. Nessa etapa todos os dados necessário da message são extraídos e validados, posteriormente sendo utilizados na chamada dos use cases.
    • requests: esse pacote é responsável por conter as estruturas que definem o das requisições recebidasbody nos handlers e também validadores para o body, headers, pathParams e queryParams.
    • responses: esse pacote é responsável por conter as estruturas que definem body que são retornados nos handlers e também padronizar as responses de sucesso e erro.
    • messages: esse pacote é responsável por conter as estruturas e validações das messages que são recebidas nos consumers.
    • dependencies: esse pacote é responsável por conter toda a lógica referente a injeção de dependências da aplicação.
  • repository: esse pacote é responsável por conter a implementação da comunicação com um determinado banco de dados. Essa implementação sempre deve seguir o contrato estabelecido em um gateway.

    • config: esse pacote é responsável por conter as configurações dos clientes dos bancos de dados utilizados na camada de repository.
  • service: esse pacote é responsável por conter a implementação da comunicação com serviços externos. Essa implementação sempre deve seguir o contrato estabelecido em um gateway.

    • config: esse pacote é responsável por conter as configurações dos clientes https, sdks, etc… utilizados na camada de service.
  • publisher: esse pacote é responsável por conter a implementação da publicação de uma mensagem em determinado tópico. Essa implementação sempre deve seguir o contrato estabelecido em um gateway.

    • config: esse pacote é responsável por conter as configurações dos clientes utilizados na camada de publisher.

Business

business: essa pacote é responsável por conter tudo aquilo que faz parte do domínio da nossa aplicação, ele é dividido nos seguinte sub pacotes.

  • usecase: esse pacote é responsável por conter a implementação dos casos de uso da nossa aplicação, ou seja, tudo aquilo referente as regras de negócio necessárias para atender as solicitações. Algumas solicitações podem necessitar de serviços externos para validar informações referentes a regra de negócio, por exemplo, verificar se um item pertence a um determinado usuário antes de realizar uma alteração. Para realizar esse tipo de operação, o use case deve "enxergar" o gateway que define o contrato desse serviço externos, ou seja, dependa sempre da abstração, nunca da implementação.

  • logic: essa é uma camada opcional, em muitas aplicações ela não necessita existir. Essa camada utilizamos quando necessitamos de tratamentos mais complexos sobre determinado usecase, por exemplo, caso tenhamos um caso de uso que de acordo com o tipo de recurso que esta recebendo necessita realizar fluxos completamente diferentes, nesse caso para não agregarmos muita complexidade a nossa camada de usecase criamos na camada logic e podemos utilizar o padrão strategy para gerenciar essa complexidade.

  • gateway: esse pacote é responsável por conter os contratos (interfaces) que definem a comunicação com serviços externos que são implementados nas camadas de repository, service e publisher. Ou seja, ele é a ponte da camada de business com a camada de infrastructure.

  • domain: esse pacote é responsável por conter o domínio da nossa aplicação, ou seja, todas as estruturas que definem entidades core. Por exemplo, user, userID, item, invoice, purchase, etc. IMPORTANTE: a camada domain pode ser usada pelas demais camadas do projeto, porém ela NUNCA deve usar outras camadas do projeto. Outro ponto importante desta camada é a preferência por evitar domínios anêmicos.

Boas práticas

Essas seção tem como objetivo listar boas práticas ao longo do desenvolvimento desse tipo de aplicação.

Testes Unitários

Outro ponto muito importante é a escrita de testes unitários, porém não acredito que seja uma vantagem escrever testes para todas as camadas. De acordo com experiências passadas, não realizaria testes da camada de infrastructure em relação ao pacotes que implementam os gateways (repository, service e publisher), pois a maioria do código contido nesses pacotes são libs de terceiros que já foram testadas e muitas vezes necessitamos mockar comportamentos que fazem nosso teste não ter uma real utilidade a não ser aumentar o "coverage".

Outro ponto importante é realizar a escrita dos testes unitário de maneira correta para evitar um excesso de complexidade. Quando falamos de testes unitário, estamos nos referindo a isolar uma unidade de código e testa-la, ou seja, se estamos testando um handler o use case chamado por esse handler durante o teste DEVE ser um mock e não o use case real. Da mesma forma, quando estivermos testando um use case todas as chamadas aos serviços externos fornecidas pelos gateways DEVEM estar chamando mocks ao longo do teste e não as implementações reais.

Testes de Integração

A escrita de testes de integração é muito importante para garantir a confiabilidade da nossa code base, evitando que errors entre a comunicação das camadas passem despercebidos. Esses testes também são importantes para assegurar o funcionamento das camadas que não realizamos os testes de unidade como mencionado anteriormente.

Esses testes possuem menos casos que os de unidade, sendo geralmente escritos para testar o caso de sucesso. Para sua escrita, levantamos a aplicação e mockamos somente as dependências externas, por exemplo

  • Caso utilizamos um banco sql, podemos levantar um banco em memória ou algo similar para utilizar ao longo do teste.

  • Caso o fluxo a ser testado realize chamadas http ao longo da execução, realizamos o mock dessas chamadas http.

Dessa forma conseguimos simular o comportamento de um cliente e garantir que a comunicação entre todas as nossas camadas estejam funcionando corretamente.

Fluxo de desenvolvimento

Uma sugestão de desenvolvimento para essa estrutura de aplicação é evitar iniciar criando toda a estrutura. Comece pelo usecase, dessa forma mantemos o foco no que realmente importa e o restante da aplicação vai crescendo de acordo com esse foco. Tendo isso em mente, podemos seguir os seguintes passos:

  1. Criar a camada de bussiness e a sub camada de usecase

  2. Comece a escrever o usecase e vá criando as demais camadas conforme o usecase vá necessitando delas. Dessa maneira, evitamos que fique código não utilizado na aplicação e que ela não fique estruturada corretamente.

DICA: Uma dica para facilitar o desenvolvimento é utilizar diagrama de fluxos, dessa forma podemos desenhar um diagrama por usecase e já visualizar exatamente o que temos que implementar. Uma ferramenta excelente para desenhar diagramas é o Mermaid.

Ponto de atenção

Esse modelo de desing separa completamente a regra de negócio de tecnologias de terceiros, mas e se nossa regra de negócio necessita de uma transacion SQL por exemplo? Não podemos ser radicais, as vezes temos que "sujar" o design da nossa aplicação para atender as regras de negócio de forma eficiente, porém devemos evitar sempre que possível.

Top comments (0)