Arquitetura limpa para bases de código React

Photo by Pierre Châtel-Innocenti on Unsplash

O objetivo desse post é explicar como aplicar Clean architecture a codebases react, tendo como contêiner de estado Redux.
Trarei dois exemplos que são aplicáveis tanto para React como para React Native.
O porquê e o que serão discutidos em seus respectivos posts.

Código fonte

Caso o tutorial não seja relevante, você pode dar uma olhada direto no código.

Introdução

Como o objetivo deste post não é explicar os conceitos de clean architecture, falarei brevemente sobre os conceitos que serão usados neste post. Caso já esteja familiarizado com os conceitos, tu podes pular esta seção.
Caso queira conhecer melhor os conceitos, recomendo os seguintes posts:
- How to write robust apps every time, using “The Clean Architecture”
- The Clean Architecture

A abordagem mais simples de react-redux pode ser ilustrada da seguinte forma:

Diagrama de comunicação react-redux.

Ao adotar Clean Architecture esta modelagem semelhante, porém com outros nomes e ganhamos dois novos termos: Entidades e Interactors*.

*Não consegui achar uma tradução boa para a palavra Interactor.
Diagrama de comunicação seguindo clean architecture

Entidades

Entidades consiste nas regras de negócio que são universais, isto é independe da aplicação que vai simular esta regra. Elas representam entidades que existem no domínio da aplicação.

Interactors / Casos de Uso

Interactors são responsáveis pelas regras de negócios específicas da aplicação, ou seja regras de negócio que levam em conta a aplicação que irá executar as regras, a preocupação do interactor é coordenar o que deve ser feito deixando em aberto o como. O como deve ser feito é responsabilidade de quem vai implementar/instanciar o interactor.

A literatura também usa o termo caso de uso para se referir ao conceito, deixando o termo Interactor para a implementação/instanciação de um caso de uso.

Adaptadores e Apresentadores

Adaptadores e Apresentadores consistem na camada que trabalha na fronteira entre a camada de domínio e camada de apresentação. São responsáveis por mapear o formato que é mais conveniente para o domínio do negócio para um formato mais conveniente para a apresentação. Sem essa camada o domínio da aplicação estaria acoplado a camada de apresentação. É essa camada também que nos permite aproveitar a lógica de negócio para diferentes tipos de aplicação.

Componentes

É tudo aquilo que vai ser mostrado ao usuário. Mais especificamente aos Presentational Components.

Primeiro exemplo: Contador

Nosso primeiro exemplo vai ser algo bem simples, implementar um contador. Devido a simplicidade do problema, não é tão claro os benefícios que Clean architecture nos traz, porém tenha calma, o próximo exemplo elucidará melhor.
A regra de negócio é simples: O contador precisa ter um teto e um piso de valor, e esses valores podem variar entre aplicações.

Entidade

Eu entendo que nem todo mundo gosta de mapear estruturas de dados como classe, o contador poderia ser simplesmente um número ou uma estrutura de dados que melhor lhe convir.

Interactor

Apesar de termos um monte de código para fazer uma coisa simples, temos o beneficio de termos a nossa lógica de negócio completamente independente de agentes externos e conseguimos configurar quais serão o teto e o piso durante a instanciação do interactor.

Redux, React-Redux e React

Daí para frente o processo é o mesmo, provavelmente você precisará instanciar o interactor de maneira diferente caso você precise customizar ele, no repositório do app de exemplo, a aplicação React Native possui teto e piso diferente da web, porém conseguem compartilhar toda lógica de negócio independente se ambas estão usando Redux ou não.

Resultado final

React e React Native compartilhando lógica de negócios através de arquitetura limpa.

Segundo Exemplo: Autenticação

Nosso segundo exemplo vai ser algo mais perto da realidade, um módulo de autenticação. As regras de negócio agora são:

  • Senhas não podem conter nada além de letras e números.
  • Usuários devem ter endereço de email válido.
  • Usuários devem ser cadastrados com o Nome completo, e devem ser em lower case.
  • Durante o cadastro, precisa verificar se já existe um outro usuário com o mesmo email.
  • A aplicação precisa usar uma dependência externa para persistir o usuário.

Entidades

Para resolver este problema mapeei as seguintes entidades: Email, Credencial e Usuário. Evitar ter um post muito grande decidi não colocar as implementações aqui, mas tu podes conferir neste LINK.

Interactor de Entrada

Para fazer o login de um usuário, precisamos de um serviço externo, normalmente uma API, que vai verificar se o usuário existe ou não e depois ver se a senha esta correta para assim permitir a entrada do usuário. O que torna o nosso interactor assíncrono e dependente de algo externo a aplicação.

Aqui fizemos uso de inversão de dependência através de expor uma interface que recebe uma credencial e retorna uma Promise que retornará um Usuário. Mais uma vez, o exemplo que propus é simples com o intuito de mostrar o conceito, caso queira um exemplo um pouco mais rebuscado tu podes conferir o Interactor de Cadastro.

Adaptador Assíncrono e com dependência externa

Como vocês devem saber, redux por si só não sabe tratar ações assíncronas e não conseguiria injetar a dependência do nosso serviço de SignIn no Interactor de entrada. Agora entra em cena Redux-Saga, e como adapta-lo ao interactor.

Mais uma vez o nosso adaptador, que neste caso é uma saga, simplesmente instancia o interactor e chama a função relativa a ação desejada. Isso na realidade, aumenta a sensação que eu costumo ter ao trabalhar com redux que é a quantidade de código boilerplate é gerado para fazer algo relativamente simples. Porém estamos deixando nossa aplicação mais maleável para no dia que tivermos coragem tirarmos de vez o redux da nossa aplicação.

Serviços

Na saga que acabamos de mostrar faz uso de uma implementação do LoginService que chamamos de SampleService. Aqui entra um ponto interessante e que foi um dos pontos que me fez acreditar em Arquitetura limpa para backends. Para a nossa lógica de negócio, pouco importa se estamos chamando uma stored procedure, fazendo uma query num banco mongo, chamando uma API SOAP ou REST. Esses aspectos importam para fins operacionais e tecnológicos, e os motivos para as tomadas de decisões podem ter mudado no meio do caminho. Deixar isso o mais abstrato possível para as regras de negócio possibilita tomarmos decisões tecnológicas mais voltadas a resultado, ao invés de voltado ao impacto negativo que essa decisão pode trazer ao app em produção.

Resultado final

React e React Native compartilhando lógica de negócios através de arquitetura limpa.

Conclusão

Provavelmente isso não vai ser o suficiente para te convencer que aumentar o grau de abstração da sua aplicação vai te trazer benefícios a médio e longo prazo. Mas quando você estiver convencido e precisar de um exemplo para consolidar os seus estudos e organizar seu codebase este post vai ser valioso. Pretendo compartilhar as motivações em outro post, quaisquer dúvidas ou feedbacks são bem vindos.