DEV Community

Cover image for Cebolas e camadas para padrões de projetos no Front-end — Parte I

Cebolas e camadas para padrões de projetos no Front-end — Parte I

Nesse texto, tenho como objetivo trazer uma alternativa de padrões de projetos front-ends, esse padrão funciona independente do framework ou biblioteca.

A estrutura proposta neste artigo utiliza alguns recursos e nomenclaturas conhecidas pela comunidade do ReactJS, mas outras são uma inspiração de outros ecossistemas como Angular e similares, a ideia é compartilhar um padrão que já adotei em alguns projetos e que serve muito bem para um monolito escalável, e com uma ótima fórmula para modelar um ecossistema que poderá evoluir para um micro-frontend em um futuro próximo.

O que vamos encontrar aqui?

  • Um projeto To-Do List com ReactJs e arquitetura onion.
  • Referências a outras publicações que irá descrever melhor alguns contextos e implementações.
  • Uma simples comparação entre uma arquitetura layered e uma onion.
  • Um relato dos projetos em que implementei essa composição com pontos positivos e negativos.

Mas antes de começar:

  • O projeto de exemplo não possui uma API para gerenciar acessos e salvar dados, ou seja, os dados são salvos localmente no navegador.
  • Essa arquitetura não é uma bala de prata, é complexa e robusta para escalar grandes projetos, para algo menor considere recursos mais objetivos como um MVC por exemplo.
  • Os nomes podem ser modificados e adaptados para cada organização e contexto.
  • É uma arquitetura conhecida no mundo back-end então, possuímos muitos posts e referência sobre ela, mas são sobre aplicações back-end.

Porque eu preciso adotar padrões de projetos?

O conceito de padrões foi primeiramente descrito por Christopher Alexander em Uma Linguagem de Padrões.

Padrões de projeto são soluções comuns para alguns problemas típicos, mas acredito que não deva ser algo que seja uma organização de arquivos que fica mais bonita ou que agrade a preferência do programador que está construindo, porque tudo isso é sempre do ponto de vista de quem implementa. Para, ser imparcial durante a definição de um bom padrão de projeto eu levo alguns pontos em consideração de que ele precisa cumprir:

  • Testabilidade — Que seja possível implementar diferentes testes (unitário, integrado, E2E, mutação, regressão e etc…), precisa ser fácil testar, a porcentagem de coverage não significa qualidade de teste e muito menos de software, porém quanto mais difícil de testar mais cenários os desenvolvedores irá deixar de testar ao longo do caminho.
  • Manutenabilidade — A alteração de algo em funcionamento, não deveria afetar muitos lugares do organismos. Abstrações e camadas podem parecer burocráticas e verbosa, mas elas garantem a manutenção, isolamento do contexto e sua sanidade no futuro.
  • Escalabilidade — Uma aplicação assim como uma cidade precisa ser preparada para abrigar muitos cidadãos (usuários) e os engenheiros que irão mantê-la (devs). Não é só escalar apenas código, é escalar a capacidade de muitas pessoas atuarem no mesmo projeto, um projeto grande com muitas pessoas, irá enfrentar problemas como: CI/CD, Ambiente de deploy, PRs e merges, rollback, estratégias de deploy e etc. Para todas as nossas decisões temos que levar isso em consideração.
  • Separação de responsabilidades (SoC — separation of concerns) — Isolar domínios e responsabilidades, permite um compartilhamento melhor de recursos sem afetar a performance e complexidade da sua aplicação.
  • Observabilidade — Se algo quebrou, eu preciso saber o que e em qual momento isso aconteceu, preciso atuar e reverter o problema de maneira rápida e eficiente, garantindo que o erro não volte mais a acontecer. Como podemos saber qual é a parte da sua aplicação que mais consome performance? Será que só usar a URL é o suficiente para entender quantos elementos e domínios temos ali e o que eles realmente fazem?
  • Evitar pastas genéricas e sem propósito, shared e utils — Quando não conseguimos separar bem nossas entidades e camadas, começamos a criar acoplamento entre os recursos, gerando a necessidade de criar pastas para shared ou utils. Funciona para o primeiro elemento, mas depois de um tempo, os desenvolvedores não irão gastar muito tempo pensando sobre o domínio das coisas, e tudo será útil ou shared.

Entre camas e cebolas.

Uma padrão de projeto de camadas tradicional (layered pattern), o fluxo de dependências entre as camadas segue de cima para baixo. Na camada superior encontramos a área de interface do usuário (User Interface), no meio o processamento das informações (Business), e na base a saída ou entrada de dados para um serviço externo (Data).

Layered architecture — Arquitetura em Camadas

Fazendo uma sim analogia com uma estrutura em React:

Exemplo de composição de componentes com React.

O grande ponto sobre esse padrão é o forte acoplamento entre as camadas, uma vez que é preciso substituir qualquer uma delas, as outras camadas relacionadas sofrem uma grande alteração ou conflito de comportamento. Ou seja, imagina que precisamos mudar a camada de Data de um Rest API para um GraphQL, a camada de business sofreria diretamente com essa mudança, além da baixa reutilização dos mesmos recursos em lugares diferentes.

Agora invertemos a ordem das dependências, colocando como centro a nossa camada de Business e as camadas de Data e User Interface como referências ao business. Temos uma forma de garantir que, o que é importante para nossa aplicação, fique segura de mudanças garantindo que as camadas externas fiquem responsáveis por lidar com o mundo exterior.

Inversão das camadas levando o business como principal referência.

Vamos mudar um pouco mais o formato do desenho:

Convertendo o desenho da camada para um primeiro formato da nossa cebola.

Dessa forma, o que temos de evidência?

  • O Foco é a parte central da nossa aplicação o business, é onde a nossa mudança de negócio e evolução irá acontecer ao longo do tempo. Em alguns casos é possível manter uma linguagem mais pura, sem muitos recursos atrelado a um framework.
  • User Interface e Data são camadas periféricas, onde os recursos são substituíveis e podem ser atrelados a um recurso ou tecnologia.

Evoluindo nossa cebola

Podemos quebrar ainda mais as camadas dividindo responsabilidades:

Onion Architecture — Arquitetura Onion

A camada de user Interface se torna:

Presentation — Essa é a camada relacionada ao dispositivo do usuário, seja para web, navegador ou mobile. Os controles de rotas, parâmetros de urls, diagramação visual da página e composição de múltiplos domínios, devem ser tratados aqui. No caso de uma aplicação de front-end os componentes de UI também se encontram aqui, ou seja tome cuidado ao utilizar contextos de domínios em componentes que são apenas UI como Button por exemplo.

Business é composta de:

Application — Essa camada do Business é considerada como operador do domínio, conhecido por alguns como os “Use Cases (Caso de usos)”, são métodos ou componentes que cumprem uma função de controlar o estado do domínio.

Domain — Já na camada de domínio, encontramos o controle das nossas principais entidades. “Todos os objetos de uma aplicação deveriam ser uma entidade?” A resposta é não necessariamente, quando você se encontra em uma situação que precisa compartilhar a mesma informação entre dois organismos distintos e vizinhos, você se esbarra no problema da Mutabilidade, logo conseguimos resolver isso com uma entidade Imutável que é parte do domínio. Se o seu domínio for um método de composição de objeto ao invés de um objeto em si também funciona, é um comportamento muito comum em uma estrutura de Functional Programing.

A camada de Data é separada em:

Persistence — Nem todas as aplicações que trabalhei precisou dessa camada, mas cada vez mais tempos aplicações front-ends que permitem o usuário utilizar recursos offline. Logo precisamos persistir um dado primeiramente no dispositivo do usuário, antes de enviar para o back-end. Essa camada abstrai a forma que persistimos essa informação, seja através de um Index DB, Local storage ou memória volátil. Novamente o nosso domínio não deveria se importar quem e como essas informações chegam ou saem.

Infrastructure — Aqui configuramos a camada de serviço e adaptadores, uma forma de criar métodos e clientes para controlar nossas chamadas com o back-end, seja ela através de Rest API, Web Assembly, GRPC ou qualquer outro recurso, novamente nosso domínio poderia apenas esperar uma promessa de dados seja como for, a camada de infraestrutura deve se resolver para passar esse acordo. Outra coisa bacana, podemos adicionar aqui transformadores e compositores, ou seja nossa entidade do domínio pode ser um objeto que para existir, precisa ser composto por uma ou mais APIs e modificações em dados, na falta de um BFF, a camada de service trabalha muito bem removendo essa composição do seu domínio e entregando um dado mais puro para as camadas de dentro da cebola.

As pastas e arquivos precisam respeitar esses nomes?

Não necessariamente, é claro que se estiver com esses nomes facilita a identificação dos contextos por aqueles que já conhecem esse padrão. Porém a adoção dos padrões é um acordo entre os desenvolvedores, e para todos os projetos que implementei, a nomenclatura dos organismos ainda foram o ponto de resistência à mudança pelos desenvolvedores Front-ends. Então minha escolha foi manter os organismos com nomes já conhecido pela comunidade, e criar um dicionário explicando onde se encaixa cada um deles e seus respectivos papéis para o projeto.

Para entender melhor como cada camada ou elemento disso se comporta e funciona, leia a parte 2 dessa publicação.

Pontos positivos

  • Essa estrutura permite tomar decisões mais tarde, ou seja, podemos desenvolver uma feature ou módulo, sem precisar esperar a API ficar pronta, conseguimos criar um mock dos dados e mudar isso no futuro sem afetar o domínio.
  • Facilita muito para testar e conseguir garantir uma boa cobertura e qualidade de test.
  • É fácil identificar a quebra de um módulo e features em pequenas atividades e PRs.
  • Exclui a necessidade de pastas como shared, utils e recursos que são uma gaveta de quinquilharias, no começo do projeto faz sentido, depois de um ano não sabemos se tudo que tem lá é realmente para ser compartilhado ou útil.
  • Essa estrutura funciona para qualquer tipo de linguagem ou tecnologia, já usei com projetos Angular, React + NextJS, React, React + Gatsby, VUE e etc.

Pontos negativos

  • A curva de aprendizado é um pouco mais demorada, é uma estrutura robusta e complexa que exige um entendimento do porquê das coisas.
  • Antes de desenvolver um domínio é preciso gastar tempo pensando como ele será, o que é relacionado ao seu contexto e o que não é.
  • Possui camadas de abstração mesmo que pareça redundante.
  • Diferente do Java que é uma linguagem que possui recursos como “Protected”, que consegue bloquear o uso indevido de algumas funcionalidades, o javascript é modo “Freestyle” onde tudo é possível, mas nem tudo deveria ser permitido. Então criar bons padrões de alias, ajuda a identificar quando alguém está usando algo de forma errada.

Referências

Código de exemplo:
https://github.com/RobsonMathias/frontend-onion-architecture

Fontes de estudos:
https://refactoring.guru/pt-br/design-patterns
https://code-maze.com/onion-architecture-in-aspnetcore
https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1

Top comments (0)