DEV Community

Rubem Vasconcelos
Rubem Vasconcelos

Posted on • Edited on

Arquitetura Limpa: Aplicando com React

Esse texto faz parte de uma série de textos sobre análise da Arquitetura Limpa aplicada com frameworks e linguagens distintas.

Os propósitos deste texto seguem alinhados com os do texto anterior, sendo eles: I. Mostrar uma divisão arquitetural de uma aplicação React usando Arquitetura Limpa; II. Guiar a implementação de novas features nesta arquitetura proposta.

Divisão Arquitetural

O passo inicial é analisar como é feita a divisão.



cypress/
src/
  data/
    protocols/
    test/
    usecases/
  domain/
    errors/
    models/
    test/
    usecases/
  infra/
    cache/
    http/
    test/
  main/
    adapters/
    config/
    decorators/
    factories/
      cache/
      decorators/
      http/
      pages/
      usecases/
    routes/
    scripts/
    index.tsx
  presentation/
    assets/
    components/
    hooks/
    pages/
    protocols/
    routes/
    styles/
    test/
  requirements/
  validation/
    errors/
    protocols/
    test/
    validators/


Enter fullscreen mode Exit fullscreen mode

Em detalhes, o propósito de cada estrutura de arquivos é o seguinte:

  • cypress: Contém os arquivos de teste end to end da aplicação (para projetos grandes, essa pasta é recomendável que seja em um projeto a parte, para que o time responsável pelos testes e2e cuide dele, uma vez que não precisa conhecer o código do projeto).
  • src: Contém todos os arquivos necessários para o sistema.
    • Data: A pasta data representa a camada de dados da Arquitetura Limpa, sendo dependente da camada de domínio. Contém as implementações das regras de negócio que são declaradas no domain.
    • Domain: Representa a camada de domínio da Arquitetura Limpa, a camada mais interna da aplicação, não apresentando dependência com nenhuma outra camada, onde contém as regras de negócio.
    • Infra: Essa pasta contém as implementações referentes ao protocolo HTTP e ao cache, também é único local onde terá acesso a dependências externas relacionadas para esses dois itens citados. Aqui também é onde está contido a maioria das bibliotecas externas.
    • Main: Corresponde a camada principal da aplicação, ponto que ocorre a integração das interfaces desenvolvidas na camada de UI, com as regras de negócio criadas nas pastas que representam as camadas mais internas da Arquitetura Limpa. Tudo isso se dá devido ao uso de padrões de projetos como Factory Method, Composite e Builder.
    • Presentation: Nesta pasta contém a parte visual da aplicação, com suas páginas, componentes, hooks, assets e estilizações.
  • Requirements: Contém os requisitos do sistema documentados.
  • Validation: Onde contém as implementações das validações utilizadas nos campos.

Diferente da abordagem com Flutter onde havia uma pasta central que se concentravam todos os testes, nessa abordagem os testes se encontram nas respectivas pastas dentro da src.

Guia de Implementação

Nesta seção será descrita uma sequência lógica recomendada para um melhor desempenho de implementação de sistemas React utilizando esta arquitetura.

Para finalidade de simplificar a explicação, não será descrito em detalhes os testes unitários. No entanto, é fortemente recomendado começar pelos testes unitários antes do desenvolvimento (TDD) de cada passo utilizando os requirements para embasar os cenários. E após finalizar os cenários, fazer o teste end to end do fluxo (se for um dos principais, ter em mente a pirâmide de testes).

A demonstração a seguir é da criação do fluxo de Login para entrar em uma aplicação.

Primeiro passo: Criar as regras de negócio na camada de domínio

Dentro de src/domain/usecases, criar o authentication.ts. Esse arquivo será uma interface que vai descrever a regra de negócio da autenticação.



import { AccountModel } from '@/domain/models/';

export interface IAuthentication {
  auth(params: Authentication.Params): Promise<Authentication.Model>;
}

export namespace Authentication {
  export type Params = {
    email: string;
    password: string;
  };

  export type Model = AccountModel;
}


Enter fullscreen mode Exit fullscreen mode

Como vemos, essa interface tem um método auth() que recebe os parâmetros Authentication.Params que são declarados num namespace abaixo - contendo o tipo dos parâmetros (email e password) e o tipo do model (AccountModel) - e espera retornar um Authentication.Model de maneira assíncrona.

O AccountModel é uma exportação nomeada do model criado em src/domain/models que representa o token que é retornado após a autenticação para persistir a sessão.



export type AccountModel = {
  accessToken: string;
};


Enter fullscreen mode Exit fullscreen mode

Segundo passo: Implementar as regras na camada de dados

Nessa camada, criamos o caso de uso para implementar a interface criada anteriormente na camada de domínio, porém dentro de src/data/usecases.

O arquivo tende a ficar como o exemplo abaixo.



import { IHttpClient, HttpStatusCode } from '@/data/protocols/http';
import { UnexpectedError, InvalidCredentialsError } from '@/domain/errors';
import { IAuthentication, Authentication } from '@/domain/usecases';

export class RemoteAuthentication implements IAuthentication {
  constructor(
    private readonly url: string,
    private readonly httpClient: IHttpClient<RemoteAuthenticationamespace.Model>
  ) {}

  async auth(
    params: Authentication.Params
  ): Promise<RemoteAuthenticationamespace.Model> {
    const httpResponse = await this.httpClient.request({
      url: this.url,
      method: 'post',
      body: params,
    });

    switch (httpResponse.statusCode) {
      case HttpStatusCode.ok:
        return httpResponse.body;
      case HttpStatusCode.unauthorized:
        throw new InvalidCredentialsError();
      default:
        throw new UnexpectedError();
    }
  }
}

export namespace RemoteAuthenticationamespace {
  export type Model = Authentication.Model;
}


Enter fullscreen mode Exit fullscreen mode

Como podemos observar, a classe RemoteAuthentication implementa a interface IAuthentication, recebendo o cliente HTTP e a url para a requisição. No método auth() ele recebe os parâmetros, e chama o httpClient passando a url, o método (nesse caso é o post) e o body (que são os parâmetros). Esse retorno é uma httpResponse do tipo referente ao Authentication.Model que tem um código de status de resposta, e que a depender do seu resultado, dá o respectivo retorno - podendo retornar o valor esperado pela requisição ou um erro.

Os códigos de status são os HTTP:



export enum HttpStatusCode {
  ok = 200,
  created = 201,
  noContent = 204,
  badRequest = 400,
  unauthorized = 401,
  forbidden = 403,
  notFound = 404,
  serverError = 500,
}


Enter fullscreen mode Exit fullscreen mode

Terceiro passo: Implementar as páginas na camada de presentation

Para simplificar o entendimento, será apresentado apenas trechos de códigos referentes a chamada do método de autenticação. A página de Login contém mais ações e detalhes que vão além da autenticação. Levar em consideração o protótipo da página abaixo para facilitar a visualização.

Login page

Em src/presentation/pages/ será criada a página de Login que é composta por componentes, métodos e funções. O componente que chama o método de autenticação é o <Button/> que está contido no formulário para pegar os valores dos inputs, conforme o trecho de código a seguir:



<form
  data-testid="loginForm"
  className={Styles.form}
  onSubmit={handleSubmit}
> 
  <Input
    autoComplete="off"
    title="Digite seu e-mail"
    type="email"
    name="email"
  />
  <Input
    autoComplete="off"
    title="Digite sua senha"
    type="password"
    name="password"
    minLength={6}
  />
  <Button
    className={Styles.loginBtn}
    type="submit"
    disabled={state.isFormInvalid}
    title="Entrar"
    data-testid="loginButton"
  />
</form>


Enter fullscreen mode Exit fullscreen mode

Ao clicar no Button, é chamado o handleSubmit() que está no onSubmit do form.



const handleSubmit = async (
    event: React.FormEvent<HTMLFormElement>
  ): Promise<void> => {
    event.preventDefault();
    try {
      const account = await authentication.auth({
        email: state.email,
        password: state.password,
      });

      setCurrentAccount(account);
      history.replace('/');
    } catch (error) {
      // Error handling here
    }
  };


Enter fullscreen mode Exit fullscreen mode

Onde o authentication.auth() clicado chamará uma factory (veremos mais a seguir) para fazer a autenticação. Nesse caso está passando os parâmetros capturados pelo input e o valor retornado da requisição é salvo no cache através do setCurrentAccount(account);.

Quarto passo: Conectar todas as camadas para que as requisições funcionem

Após tudo implementado, agora basta conectar todas as partes. Para isso, é utilizado o padrão de projeto Factory Method.

Dentro de src/main/factories/usecases, criamos a factory do caso de uso que está sendo implementado. No caso desse exemplo, é o relacionado a autenticação.

É criado o makeRemoteAuthentication, que retorna o RemoteAuthentication que recebe como parâmetro a factory que cria a URL e a factory do Http Client. É passado como parâmetro a URL da API que deseja requisitar junto da factory que cria a URL. No exemplo é a URL que finaliza com /login.



import { RemoteAuthentication } from '@/data/usecases/';
import { IAuthentication } from '@/domain/usecases';
import { makeAxiosHttpClient, makeApiUrl } from '@/main/factories/http';

export const makeRemoteAuthentication = (): IAuthentication => {
  const remoteAuthentication = new RemoteAuthentication(
    makeApiUrl('/login'),
    makeAxiosHttpClient()
  );

  return remoteAuthentication;
};


Enter fullscreen mode Exit fullscreen mode

Após isso, em src/main/factories/pages, é criada a pasta para as factories do Login. Em páginas com formulário também são injetadas validações, porém como o foco desse texto são as integrações, deixaremos esse ponto de fora da explicação.



import React from 'react';
import { Login } from '@/presentation/pages';
import { makeRemoteAuthentication } from '@/main/factories/usecases/';

const makeLogin: React.FC = () => {
  const remoteAuthentication = makeRemoteAuthentication();

  return (
    <Login
      authentication={remoteAuthentication}
    />
  );
};

export default makeLogin;


Enter fullscreen mode Exit fullscreen mode

É criada uma const makeLogin que representa a factory. Ela possui o makeRemoteAuthentication que é injetado dentro da página de Login criada na camada de presentation para que a página tenha acesso a essas requisições.

Quinto passo: Aplicar a página criada na aplicação

Por fim, é necessário adicionar a factory do Login nas rotas da aplicação para que ela seja acessada pelos usuários.

No arquivo router.tsx que fica localizado em src/main/routes, adicionar a factory da página criada dentro do Switch do BrowserRouter. É passado no path a rota, no caso é a /login, e a página no component, que no caso é o ponteiro para a factory makeLoginPage. Essa lógica é utilizada com todas as outras páginas, alterando apenas de Route para PrivateRoute caso a rota seja autenticada. O código parecido com este abaixo.



const Router: React.FC = () => {
return (
<ApiContext.Provider
value={{
setCurrentAccount: setCurrentAccountAdapter,
getCurrentAccount: getCurrentAccountAdapter,
}}
>
<BrowserRouter>
<Switch>
<Route exact path="/login" component={makeLogin} />
<PrivateRoute exact path="/" component={makeDashboard} />
</Switch>
</BrowserRouter>
</ApiContext.Provider>
);
};

Enter fullscreen mode Exit fullscreen mode




Conclusão

A Arquitetura Limpa apesar de ser um pouco complexa de se entender e implementar no começo - e até parecer redundante -, as abstrações são necessárias. São aplicados diversos padrões de projetos para garantir a qualidade e independência do código, facilitando a evolução e manutenção independente de framework. Em casos como esse, se desejar mudar o framework de React para Angular ou qualquer outro baseado em Typescript, basta mudar a camada de apresentação e fazer ajustes nas dependências.

Seguir o processo de desenvolvimento e entender o porquê está fazendo de tal maneira facilita a produção do código. Após um tempo acaba sendo feito de maneira natural, pois tem um processo linear de desenvolvimento: I. Caso de uso na camada de domínio; II. Caso de uso na camada de dados; III. Criação das interfaces na camada de presentation; IV. Criação das factories para integrar todas as camadas na camada principal; V. E a chamada da factory principal nas rotas da aplicação.

Pelo exemplo ter muitas partes abstraídas, é recomendável a leitura do código das partes ocultas para uma maior compreensão. Nesse repositório você consegue ter acesso a códigos abstraídos similares ao desse exemplo dado.

Você também pode ter acesso a essa arquitetura rodando apenas o comando npx @rubemfsv/clean-react-app my app, similar ao create-react-app, mas de uma maneira mais limpa e escalável.

Referências

  • Rodrigo Manguinho https://github.com/rmanguinho/clean-react
  • MARTIN, Robert C. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. 1st. ed. USA: Prentice Hall Press, 2017. ISBN 0134494164.

Top comments (1)

Collapse
 
reginadiana profile image
Diana Regina

Parabéns pelo artigo!! Só notei um ponto para correção, na frase:

"para projetos grandes, essa pasta é comendável que seja"

Acho que aqui você queria usar "recomendável"