Você está na página 1de 10

Open in app Get started

Eduardo Morôni Follow

Jun 26, 2018 · 6 min read

Save

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.

eduardomoroni/react-clean-architecture
react-clean-architecture - A realistic approach to implement clean architecture
on react codebases Open in app Get started

github.com

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.
Open in app Get started

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

1 export class Counter {


2 count: number;
3
4 constructor(startNumber: number) {
5 this.count = startNumber;
6 }
7
8 increment(qty?: number) {
9 this.count += qty ? qty : 1;
10 }
11
12 decrement(qty?: number) {
13 this.count -= qty ? qty : 1;
14 }
15 }

counterEntity.ts
hosted with ❤ by GitHub view raw

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.
Open in app Get started

Interactor

1 import { Counter } from "../entities";


2
3 export class CounterInteractor {
4 higherBound: number = 10;
5 counter: Counter;
6
7 constructor(
8 startNumber: number,
9 higherBound: number = 10
10 ) {
11 this.counter = new Counter(startNumber);
12 this.higherBound = higherBound;
13 }
14
15 increment(qty?: number): Counter {
16 this.counter.increment(qty);
17
18 if (this.counter.count >= this.higherBound) {
19 this.counter = new Counter(this.higherBound);
20 }
21
22 return this.counter;
23 }
24
25 decrement(qty?: number): Counter {
26 // Omitted for simplicity
27 }
28 }

counterInteractor.ts
hosted with ❤ by GitHub view raw

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.
1 // Rest of the file omitted
2 Open in app Get started
3 const incrementReducer = (
4 counter: StateSliceType,
5 action: ActionType,
6 ): StateSliceType => {
7 const interactor = new CounterInteractor(counter);
8 interactor.increment(action.qty);
9 return new Counter(interactor.counter.count);
10 };
11
12 export const counterReducer = (
13 state: StateSliceType = INITIAL_STATE,
14 action: ActionType,
15 ): StateSliceType => {
16 switch (action.type) {
17 case INCREMENT:
18 return incrementReducer(state, action);
19 case DECREMENT:
20 return decrementReducer(state, action);
21 default:
22 return state;
23 }
24 };

counterReducer.ts
hosted with ❤ by GitHub view raw

Resultado final
Open in app Get started

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
Open in app Get started

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.

1 import { Credential, User } from "../entities";


2
3 export interface SignInService {
4 signInWithCredential: (credential: Credential) => Promise<User>;
5 }
6
7 export class SignInInteractor {
8 signInService: SignInService;
9
10 constructor(signInService: SignInService) {
11 this.signInService = signInService;
12 }
13
14 async signIn(credential: Credential): Promise<User> {
15 return this.signInService.signInWithCredential(credential);
16 }
17 }

SignInInteractor.ts
hosted with ❤ by GitHub view raw

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.

1 import { all, call, put, takeLatest } from "redux-saga/effects";


2 import { Credential, User } from ../../entities ;
3 import { updateUserAction } from "./user";
Open in app Get started
4 import { SignInInteractor } from "../../useCases";
5 import { SampleService } from "../../services";
6
7 export const SIGN_IN = "user/saga/sign_in";
8
9 interface SignInActionType {
10 type: string;
11 credential: Credential;
12 }
13
14 export const signInAction = (credential: Credential): SignInActionType => ({
15 type: SIGN_IN,
16 credential,
17 });
18
19 function* signInSaga(action: SignInActionType) {
20 const { credential } = action;
21 try {
22 const service = new SampleService();
23 const interactor = new SignInInteractor(service);
24
25 const user = yield interactor.signIn(credential);
26 yield put(updateUserAction(user)); // This action simplily saves User into state
27 } catch (error) {
28 console.error(error);
29 // DO SOMETHING ELSE
30 }
31 }
32
33 export function* rootSaga() {
34 yield all([takeLatest(SIGN_IN, signInSaga)]);
35 }

SignInSaga.ts
hosted with ❤ by GitHub view raw

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,
Open in app pouco importa
Get started

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
Get started
Open in app

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.

About Help Terms Privacy

Get the Medium app

Você também pode gostar