paint-brush
Testando uma arquitetura limpa em um aplicativo front-end - faz sentido?por@playerony
10,637 leituras
10,637 leituras

Testando uma arquitetura limpa em um aplicativo front-end - faz sentido?

por Paweł Wojtasiński21m2023/05/01
Read on Terminal Reader

Muito longo; Para ler

Os desenvolvedores de front-end enfrentam o desafio de criar arquiteturas escaláveis e sustentáveis. Muitas das ideias arquitetônicas propostas podem nunca ter sido implementadas em ambientes de produção do mundo real. Este artigo tem como objetivo fornecer aos desenvolvedores de front-end as ferramentas necessárias para navegar no mundo em constante evolução do desenvolvimento de sites.
featured image - Testando uma arquitetura limpa em um aplicativo front-end - faz sentido?
Paweł Wojtasiński HackerNoon profile picture

À medida que o cenário digital evolui, também evolui a complexidade dos sites modernos. Com uma demanda crescente por melhor experiência do usuário e recursos avançados, os desenvolvedores de front-end enfrentam o desafio de criar arquiteturas escaláveis, sustentáveis e eficientes.


Entre a infinidade de artigos e recursos disponíveis sobre arquitetura frontend, um número significativo é focado na Arquitetura Limpa e sua adaptação. De fato, mais de 50% dos quase 70 artigos pesquisados discutem a Arquitetura Limpa no contexto do desenvolvimento front-end.


Apesar da riqueza de informações, uma questão gritante persiste: muitas das ideias arquitetônicas propostas podem nunca ter sido implementadas em ambientes de produção do mundo real. Isso gera dúvidas sobre sua eficácia e aplicabilidade em cenários práticos.


Impulsionado por essa preocupação, embarquei em uma jornada de seis meses para implementar a Clean Architecture no frontend, permitindo-me confrontar a realidade dessas ideias e separar o joio do trigo.


Neste artigo, compartilharei minhas experiências e insights dessa jornada, oferecendo um guia abrangente sobre como implementar com sucesso a Arquitetura Limpa no frontend.


Ao esclarecer os desafios, as melhores práticas e as soluções do mundo real, este artigo visa fornecer aos desenvolvedores de front-end as ferramentas necessárias para navegar no mundo em constante evolução do desenvolvimento de sites.

Estruturas

No ecossistema digital em rápida evolução de hoje, os desenvolvedores têm muitas opções quando se trata de estruturas de front-end. Essa abundância de opções resolve vários problemas e simplifica o processo de desenvolvimento.


No entanto, também leva a debates intermináveis entre os desenvolvedores, cada um alegando que seu framework preferido é superior aos outros. A verdade é que, em nosso mundo acelerado, novas bibliotecas JavaScript surgem diariamente e estruturas são introduzidas quase mensalmente.


Para manter a flexibilidade e a adaptabilidade em um ambiente tão dinâmico, precisamos de uma arquitetura que transcenda estruturas e tecnologias específicas.


Isso é particularmente crucial para empresas de produtos ou contratos de longo prazo que envolvam manutenção, onde mudanças de tendências e avanços tecnológicos devem ser acomodados.


Ser independente dos detalhes, como frameworks, nos permite focar no produto em que estamos trabalhando e nos preparar para mudanças que possam surgir durante seu ciclo de vida.


Não tema; este artigo pretende dar uma resposta a este dilema.

Cooperação de equipe Fullstack

Em minha busca para implementar a Arquitetura Limpa no frontend, trabalhei em estreita colaboração com vários desenvolvedores fullstack e backend para garantir que a arquitetura fosse compreensível e de fácil manutenção, mesmo para aqueles com experiência mínima em frontend.


Portanto, um dos principais requisitos de nossa arquitetura é sua acessibilidade para desenvolvedores de back-end que podem não ser versados nas complexidades do front-end, bem como desenvolvedores de full-stack que podem não ter ampla experiência em front-end.


Ao promover a cooperação perfeita entre as equipes de front-end e back-end, a arquitetura visa preencher a lacuna e criar uma experiência de desenvolvimento unificada.

Fundações teóricas

Infelizmente, para construir coisas incríveis, precisamos obter algum conhecimento prévio. Uma compreensão clara dos princípios subjacentes não apenas facilitará o processo de implementação, mas também garantirá que a arquitetura siga as melhores práticas de desenvolvimento de software.


Nesta seção, apresentaremos três conceitos-chave que formam a base de nossa abordagem arquitetônica: princípios SOLID , arquitetura limpa (que na verdade vem dos princípios SOLID) e design atômico . Se você tem uma forte opinião sobre essas áreas, pode pular esta seção.

Princípios SÓLIDOS

SOLID é um acrônimo que representa cinco princípios de design que orientam os desenvolvedores na criação de software escalável, sustentável e modular:


  • Princípio de Responsabilidade Única (SRP) : Este princípio afirma que uma classe deve ter apenas um motivo para mudar, o que significa que deve ter uma única responsabilidade. Ao aderir ao SRP, os desenvolvedores podem criar um código mais focado, sustentável e testável.


  • Princípio Aberto/Fechado (OCP) : De acordo com o OCP, as entidades de software devem ser abertas para extensão, mas fechadas para modificação. Isso significa que os desenvolvedores devem ser capazes de adicionar novas funcionalidades sem alterar o código existente, reduzindo o risco de introdução de bugs.


  • Princípio de Substituição de Liskov (LSP) : O LSP afirma que objetos de uma classe derivada devem ser capazes de substituir objetos da classe base sem afetar a correção do programa. Este princípio promove o uso adequado de herança e polimorfismo.


  • Princípio de Segregação de Interface (ISP) : O ISP enfatiza que os clientes não devem ser forçados a depender de interfaces que não usam. Ao criar interfaces menores e mais focadas, os desenvolvedores podem garantir uma melhor organização e manutenção do código.


  • Princípio de Inversão de Dependência (DIP) : DIP encoraja os desenvolvedores a depender de abstrações ao invés de implementações concretas. Esse princípio promove uma base de código mais modular, testável e flexível.


Se você gostaria de explorar este tópico com mais profundidade, o que eu recomendo fortemente que você faça, então não há problema. No entanto, por enquanto, o que apresentei é suficiente para ir mais longe.


E o que o SOLID nos dá em termos deste artigo?

Arquitetura Limpa

Robert C. Martin, baseado nos princípios SOLID e em sua vasta experiência no desenvolvimento de diversas aplicações, propôs o conceito de Clean Architecture. Ao discutir esse conceito, o diagrama abaixo é frequentemente referenciado para representar visualmente sua estrutura:



Então, Clean Architecture não é um conceito novo; tem sido amplamente utilizado em vários paradigmas de programação, incluindo programação funcional e desenvolvimento de back-end.


Bibliotecas como Lodash e várias estruturas de back-end adotaram essa abordagem arquitetônica, que está enraizada nos princípios SOLID.


A Arquitetura Limpa enfatiza a separação de preocupações e a criação de camadas independentes e testáveis dentro de um aplicativo, com o objetivo principal de tornar o sistema fácil de entender, manter e modificar.


A arquitetura é organizada em círculos ou camadas concêntricas; cada um tendo limites claros, dependências e responsabilidades:


  • Entidades : Esses são os principais objetos de negócios e regras dentro do aplicativo. As entidades geralmente são objetos simples que representam os conceitos essenciais ou estruturas de dados no domínio, como usuários, produtos ou pedidos.


  • Casos de Uso : Também conhecidos como Interactores, os Casos de Uso definem as regras de negócio específicas da aplicação e orquestram a interação entre as Entidades e os sistemas externos. Os Casos de Uso são responsáveis por implementar a funcionalidade principal do aplicativo e devem ser independentes das camadas externas.


  • Adaptadores de interface : esses componentes atuam como uma ponte entre as camadas interna e externa, convertendo dados entre o caso de uso e os formatos do sistema externo. Adaptadores de interface incluem repositórios, apresentadores e controladores, que permitem que o aplicativo interaja com bancos de dados, APIs externas e estruturas de interface do usuário.


  • Estruturas e drivers : essa camada externa compreende os sistemas externos, como bancos de dados, estruturas de interface do usuário e bibliotecas de terceiros. Frameworks e Drivers são responsáveis por fornecer a infraestrutura necessária para executar o aplicativo e implementar as interfaces definidas nas camadas internas.


A Arquitetura Limpa promove o fluxo de dependências das camadas externas para as camadas internas, garantindo que a lógica principal do negócio permaneça independente das tecnologias ou estruturas específicas usadas.


Isso resulta em uma base de código flexível, passível de manutenção e testável que pode se adaptar facilmente às mudanças de requisitos ou pilhas de tecnologia.

Projeto Atômico

O Atomic Design é uma metodologia que organiza os componentes da interface do usuário dividindo as interfaces em seus elementos mais básicos e, em seguida, remontando-os em estruturas mais complexas. Brad Frost introduziu o conceito pela primeira vez em 2008 em um artigo intitulado "Atomic Design Methodology".


Aqui está um gráfico mostrando o conceito de Atomic Design:



É composto por cinco níveis distintos:


  • Átomos : As unidades menores e indivisíveis da interface, como botões, entradas e rótulos.


  • Moléculas : grupos de átomos que funcionam juntos, formando componentes de interface do usuário mais complexos, como formulários ou barras de navegação.


  • Organismos : Combinações de moléculas e átomos que criam seções distintas da interface, como cabeçalhos ou rodapés.


  • Modelos : representam o layout e a estrutura de uma página, fornecendo um esqueleto para a colocação de organismos, moléculas e átomos.


  • Páginas : instâncias de modelos preenchidas com conteúdo real, mostrando a interface final.


Ao adotar o Atomic Design, os desenvolvedores podem colher vários benefícios, como modularidade, reusabilidade e uma estrutura clara para os componentes de UI, pois exige que sigamos a abordagem do Design System, mas esse não é o tópico deste artigo, então continue.

Estudo de caso: NotionLingo

A fim de desenvolver uma perspectiva bem informada sobre Arquitetura Limpa para desenvolvimento de front-end, embarquei em uma jornada para criar um aplicativo. Durante um período de seis meses, ganhei informações e experiências valiosas enquanto trabalhava neste projeto.


Consequentemente, os exemplos fornecidos ao longo deste artigo são extraídos de minha experiência prática com o aplicativo. Para manter a transparência, todos os exemplos são derivados de código acessível publicamente.


Você pode explorar o resultado final visitando o repositório em https://github.com/Levofron/NotionLingo .

Implementação de Arquitetura Limpa

Como mencionado anteriormente, existem inúmeras implementações de Arquitetura Limpa disponíveis online. No entanto, alguns elementos comuns podem ser identificados nessas implementações:


  • Camada de domínio : o núcleo de nosso aplicativo, abrangendo modelos relacionados a negócios, casos de uso e operações.


  • Camada de API : responsável por interagir com as APIs do navegador.


  • Camada do repositório : serve como uma ponte entre o domínio e as camadas da API, fornecendo um espaço para mapear os tipos de API para nossos tipos de domínio.


  • Camada de interface do usuário : acomoda nossos componentes, formando a interface do usuário.


Ao entender essas semelhanças, podemos apreciar a estrutura fundamental da Clean Architecture e adaptá-la às nossas necessidades específicas.

Domínio

A parte principal do nosso aplicativo contém:


  • Casos de uso : Os casos de uso descrevem as regras de negócios para várias operações, como salvar, atualizar e buscar dados. Por exemplo, um caso de uso pode envolver a obtenção de uma lista de palavras do Notion ou o aumento da frequência diária do usuário para palavras aprendidas.


    Essencialmente, os casos de uso lidam com as tarefas e processos do aplicativo de uma perspectiva de negócios, garantindo que o sistema funcione de acordo com os objetivos desejados.


  • Modelos : os modelos representam as entidades de negócios dentro do aplicativo. Eles podem ser definidos usando interfaces TypeScript, garantindo que estejam alinhados com as necessidades e requisitos de negócios.


    Por exemplo, se um caso de uso envolver a busca de uma lista de palavras do Notion, você precisará de um modelo para descrever com precisão a estrutura de dados dessa lista, respeitando as regras e restrições de negócios apropriadas.


  • Operações : às vezes, definir certas tarefas como casos de uso pode não ser viável, ou você pode querer criar funções reutilizáveis que possam ser empregadas em várias partes do seu domínio. Por exemplo, se você precisar escrever uma função para pesquisar uma palavra de noção pelo nome, é aqui que essas operações devem residir.


    As operações são úteis para encapsular a lógica específica do domínio que pode ser compartilhada e utilizada em vários contextos dentro do aplicativo.


  • Interfaces de repositório : os casos de uso requerem um meio para acessar os dados. De acordo com o Princípio de Inversão de Dependência, a camada de domínio não deve depender de nenhuma outra camada (enquanto as outras camadas dependem dela); portanto, esta camada define as interfaces para os repositórios.


    É importante observar que ele especifica as interfaces, não os detalhes de implementação. Os próprios repositórios utilizam o Repository Pattern, que é independente da fonte de dados real e enfatiza a lógica para buscar ou enviar dados de e para essas fontes.


    É importante mencionar que um único repositório pode implementar várias APIs e um único Caso de Uso pode utilizar vários repositórios.

API

Essa camada é responsável pelo acesso aos dados e pode se comunicar com várias fontes conforme necessário. Considerando que estamos desenvolvendo um aplicativo front-end, essa camada servirá principalmente como um wrapper para as APIs do navegador.


Isso inclui APIs para REST, armazenamento local, IndexedDB, síntese de voz e muito mais.


É importante observar que, se você deseja gerar tipos de OpenAPI e clientes HTTP, a camada de API é o local ideal para colocá-los. Dentro desta camada, temos:


  • Adaptador de API : O Adaptador de API é um adaptador especializado para APIs de navegador utilizadas em nosso aplicativo. Este componente gerencia chamadas REST e comunicação com a memória do aplicativo ou qualquer outra fonte de dados que você deseja usar.


    Você pode até criar e implementar seu próprio sistema de armazenamento de objetos, se desejar. Ao ter um adaptador de API dedicado, você pode manter uma interface consistente para interagir com várias fontes de dados, tornando mais fácil atualizá-los ou alterá-los conforme necessário.


  • Tipos : este é um local para todos os tipos relacionados à sua API. Esses tipos não estão diretamente relacionados ao domínio, mas servem como descrições das respostas brutas recebidas da API. Na próxima camada, esses tipos serão essenciais para o mapeamento e processamento adequados.

Repositório

A camada de repositório desempenha um papel crucial na arquitetura do aplicativo, gerenciando a integração de várias APIs, mapeando tipos específicos de API para tipos de domínio e incorporando operações para transformar dados.


Se você deseja combinar a API de síntese de voz com armazenamento local, por exemplo, este é o lugar perfeito para fazê-lo. Esta camada contém:


  • Implementação do repositório : são implementações concretas das interfaces declaradas na camada de domínio. Eles são capazes de trabalhar com múltiplas fontes de dados, garantindo flexibilidade e adaptabilidade dentro do aplicativo.


  • Operações : podem ser chamadas de mapeadores, transformadores ou auxiliares. Nesse contexto, operações é um termo adequado. Este diretório contém todas as funções responsáveis por mapear as respostas brutas da API para seus tipos de domínio correspondentes, garantindo que os dados sejam estruturados adequadamente para uso dentro do aplicativo.

Adaptador


A camada adaptadora é responsável por orquestrar as interações entre essas camadas e vinculá-las. Esta camada contém apenas módulos responsáveis por:


  • Injeção de Dependência : A camada do Adaptador gerencia as dependências entre as camadas API, Repositório e Domínio. Ao lidar com a injeção de dependência, a camada do Adaptador garante uma separação limpa de preocupações e promove a reutilização eficiente do código.


  • Organização do Módulo : A camada Adaptador organiza a aplicação em módulos com base em suas funcionalidades (por exemplo, armazenamento local, REST, síntese de voz, Supabase). Cada módulo encapsula uma funcionalidade específica, fornecendo uma estrutura limpa e modular para o aplicativo.


  • Construindo Ações : A camada do Adaptador constrói ações combinando casos de uso da camada de Domínio com os repositórios apropriados. Essas ações servem como pontos de entrada para o aplicativo interagir com as camadas subjacentes.

Apresentação

A camada de apresentação é responsável por renderizar a interface do usuário (UI) e manipular as interações do usuário com o aplicativo. Ele aproveita o adaptador, o domínio e as camadas compartilhadas para criar uma IU funcional e interativa.


A camada de apresentação emprega a metodologia Atomic Design para organizar seus componentes, resultando em um aplicativo escalável e de fácil manutenção. No entanto, esta camada não será o foco principal deste artigo, pois não é o assunto principal em termos de implementação de Arquitetura Limpa.

Compartilhado

Um local designado é necessário para todos os elementos comuns, como utilitários centralizados, configurações e lógica compartilhada. No entanto, não vamos nos aprofundar muito nessa camada neste artigo.


Vale a pena mencionar apenas para fornecer uma compreensão de como os componentes comuns são gerenciados e compartilhados em todo o aplicativo.

Estratégias de teste para cada camada

Agora, antes de mergulhar na codificação, é essencial discutir os testes. Garantir a confiabilidade e correção de seu aplicativo é vital e é crucial implementar uma estratégia de teste robusta para cada camada da arquitetura.


  • Camada de domínio : o teste de unidade é o principal método para testar a camada de domínio. Concentre-se em testar modelos de domínio, regras de validação e lógica de negócios, garantindo que eles se comportem corretamente sob várias condições. Adote o desenvolvimento orientado a testes (TDD) para conduzir o design de seus modelos de domínio e confirme se sua lógica de negócios é sólida.


  • Camada de API : teste a camada de API usando testes de integração. Esses testes devem se concentrar em garantir que a API interaja corretamente com serviços externos e que as respostas sejam formatadas corretamente. Utilize ferramentas como estruturas de teste automatizadas, como Jest, para simular chamadas de API e validar as respostas.


  • Camada de Repositório : Para a camada de repositório, você pode usar uma combinação de testes de unidade e integração. Os testes de unidade podem ser usados para testar métodos de repositório individuais, enquanto os testes de integração devem se concentrar em verificar se os repositórios interagem corretamente com suas APIs.


  • Camada do Adaptador : Os testes de unidade são adequados para testar a camada do adaptador. Esses testes devem garantir que os adaptadores injetem corretamente as dependências e gerenciem as transformações de dados entre as camadas. Zombar das dependências, como API ou camadas de repositório, pode ajudar a isolar a camada do adaptador durante o teste.


Ao implementar uma estratégia de teste abrangente para cada camada da arquitetura, você pode garantir a confiabilidade, correção e capacidade de manutenção de seu aplicativo, reduzindo a probabilidade de introdução de bugs durante o desenvolvimento.


No entanto, se você estiver construindo um aplicativo pequeno, os testes de integração na camada do adaptador devem ser suficientes.

Vamos codificar algo

Tudo bem, agora que você tem uma compreensão sólida da Arquitetura Limpa e talvez até tenha formado sua própria opinião sobre ela, vamos nos aprofundar um pouco mais e explorar algum código real.


Lembre-se de que apresentarei apenas um exemplo simples aqui; no entanto, se você estiver interessado em exemplos mais detalhados, sinta-se à vontade para explorar meu repositório GitHub mencionado no início deste artigo.


Na "vida real", a Clean Architecture realmente brilha em grandes aplicativos de nível empresarial, embora possa ser um exagero para projetos menores. Dito isso, vamos ao que interessa.


Usando meu aplicativo como exemplo, demonstrarei como realizar uma chamada de API para buscar sugestões de dicionário para uma determinada palavra. Esse terminal de API específico recupera uma lista de significados e exemplos por web scraping de dois sites.


Do ponto de vista comercial, esse terminal é crucial para a exibição "Localizar palavra", que permite aos usuários pesquisar uma palavra específica. Depois que o usuário encontra a palavra e faz login, ele pode adicionar as informações extraídas da Web ao banco de dados do Notion.

Estrutura de pastas

Para começar, devemos estabelecer uma estrutura de pastas que reflita com precisão as camadas que discutimos anteriormente. A estrutura deve se parecer com o seguinte:


 client ├── adapter ├── api ├── domain ├── presentation ├── repository └── shared


O diretório do cliente serve a um propósito semelhante ao da pasta "src" em muitos projetos. Neste projeto específico do Next.js, adotei a convenção de nomear a pasta front-end como "cliente" e a pasta back-end como "servidor".


Essa abordagem permite uma distinção clara entre os dois principais componentes do aplicativo.

Subdiretórios

Escolher a estrutura de pastas correta para o seu projeto é, de fato, uma decisão crucial que deve ser tomada no início do processo de desenvolvimento. Diferentes desenvolvedores têm suas próprias preferências e abordagens quando se trata de organizar recursos.


Alguns podem agrupar recursos por nomes de página, outros podem seguir as convenções de nomenclatura de subdiretórios geradas pelo OpenAPI e, ainda, outros podem acreditar que seu aplicativo é muito pequeno para justificar qualquer uma dessas soluções.


A chave é escolher uma estrutura que melhor atenda às necessidades específicas e à escala do seu projeto, mantendo uma organização clara e sustentável dos recursos.


Estou no terceiro grupo, então minha estrutura fica assim:


 client ├── adapter │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ └── supabase ├── api │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ └── supabase ├── domain │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ ├── supabase └── repository ├── local-storage ├── rest ├── speech-synthesis └── supabase


Decidi omitir as camadas compartilhada e de apresentação neste artigo, pois acredito que aqueles que desejam se aprofundar podem consultar meu repositório para obter mais informações. Agora, vamos prosseguir com alguns exemplos de código para ilustrar como a Arquitetura Limpa pode ser aplicada em um aplicativo front-end.

Definição de Domínio

Vamos considerar nossos requisitos. Como usuário, gostaria de receber uma lista de sugestões, incluindo seus significados e exemplos. Portanto, uma única sugestão de dicionário pode ser modelada da seguinte forma:


 interface DictionarySuggestion { example: string; meaning: string; }


Agora que descrevemos uma única sugestão de dicionário, é importante mencionar que às vezes a palavra obtida por meio do web scraping difere ou é corrigida em relação ao que o usuário digitou. Para acomodar isso, usaremos a versão corrigida posteriormente em nosso aplicativo.


Consequentemente, precisamos definir uma interface que inclua uma lista de sugestões de dicionário e correções de palavras. A interface final fica assim:


 export interface DictionarySuggestions { suggestions: DictionarySuggestion[]; word: string; }


Estamos exportando essa interface, e é por isso que a palavra-chave export está incluída.

Interface do repositório

Temos nosso modelo e agora é hora de colocá-lo em uso.


 import { DictionarySuggestions } from './rest.models'; export interface RestRepository { getDictionarySuggestions: (word: string) => Promise<DictionarySuggestions | null>; }


Neste ponto, tudo deve estar claro. É importante observar que não estamos discutindo a API aqui! A estrutura do repositório em si é bastante simples: apenas um objeto com alguns métodos, onde cada método retorna dados de um tipo específico de forma assíncrona.


Lembre-se de que o repositório sempre retorna dados no formato de modelo de domínio.

Caso de Uso

Agora, vamos definir nossa regra de negócios como um caso de uso. O código fica assim:


 export type GetDictionarySuggestionsUseCaseUseCase = UseCaseWithSingleParamAndPromiseResult< string, DictionarySuggestions | null >; export const getDictionarySuggestionsUseCase = ( restRepository: RestRepository, ): GetDictionarySuggestionsUseCaseUseCase => ({ execute: (word) => restRepository.getDictionarySuggestions(word), });


A primeira coisa a observar é a lista de tipos comuns usados para definir casos de uso. Para conseguir isso, criei um arquivo use-cases.types.ts no diretório do domínio:


 domain ├── local-storage ├── rest ├── speech-synthesis ├── supabase └── use-cases.types.ts


Isso me permite compartilhar facilmente tipos para casos de uso entre meus subdiretórios. A definição de UseCaseWithSingleParamAndPromiseResult se parece com isto:


 export interface UseCaseWithSingleParamAndPromiseResult<TParam, TResult> { execute: (param: TParam) => Promise<TResult>; }


Essa abordagem ajuda a manter a consistência e a capacidade de reutilização dos tipos de casos de uso na camada de domínio.


Você pode estar se perguntando por que precisamos da função execute . Aqui, temos uma fábrica que retorna o caso de uso real.


Essa escolha de design se deve ao fato de que não queremos fazer referência à implementação do repositório diretamente no código do caso de uso, nem queremos que o repositório seja usado por uma importação. Essa abordagem nos permite aplicar facilmente a injeção de dependência posteriormente.


Ao usar o padrão de fábrica e a função execute , podemos manter os detalhes de implementação do repositório separados do código do caso de uso, o que melhora a modularidade e a capacidade de manutenção do aplicativo.


Essa abordagem segue o Princípio de Inversão de Dependência, onde a camada de domínio não depende de nenhuma outra camada, e permite maior flexibilidade na hora de trocar diferentes implementações de repositório ou modificar a arquitetura do aplicativo.

Definição de API

Primeiro, vamos definir nossa interface:


 export interface RestApi { getDictionarySuggestions: (word: string) => Promise<AxiosResponse<DictionarySuggestions>>; }


Como você pode ver, a definição dessa função na interface é muito parecida com a do repositório. Como o tipo de domínio já descreve a resposta, não há necessidade de recriar o mesmo tipo.


É importante observar que nossa API retorna dados brutos, e é por isso que retornamos o AxiosResponse<DictionarySuggestions> completo. Ao fazer isso, mantemos uma separação clara entre a API e as camadas de domínio, permitindo mais flexibilidade na manipulação e transformação de dados.


A implementação desta API fica assim:


 export const getRestApi = (axiosInstance: AxiosInstance): RestApi => ({ getDictionarySuggestions: async (word: string) => { const encodedCurrentDate = encodeURIComponent(word); const response = await axiosInstance.get( `${RestEndpoints.GET_DICTIONARY_SUGGESTIONS}?word=${encodedCurrentDate}`, ); return response; } });


Neste ponto, as coisas ficam mais interessantes. O primeiro aspecto importante a discutir é a injeção de nosso axiosInstance . Isso torna nosso código muito flexível e nos permite construir testes sólidos facilmente. Este também é o local onde lidamos com a codificação ou análise dos parâmetros de consulta.


No entanto, você também pode executar outras ações aqui, como cortar a string de entrada. Ao injetar o axiosInstance , mantemos uma clara separação de preocupações e garantimos que a implementação da API seja adaptável a diferentes cenários ou mudanças nos serviços externos.

Implementação do Repositório

Como nossa interface já está definida pelo domínio, basta implementar nosso repositório. Assim, a implementação final fica assim:

 export const getRestRepository = (restApi: RestApi): RestRepository => ({ getDictionarySuggestions: async (word) => { const { data } = await restApi.getDictionarySuggestions(word); if (!data?.suggestions?.length) { return null; } return formatDictionarySuggestions(data); } });


Um aspecto importante a ser mencionado está relacionado às APIs. Nosso getRestRepository nos permite passar um restApi previamente definido. Isso é vantajoso porque, como mencionado anteriormente, permite testes mais fáceis. Podemos examinar brevemente formatDictionarySuggestions :


 export const formatDictionarySuggestions = ({ suggestions, word, }: DictionarySuggestions): DictionarySuggestions => { const cleanedWord = cleanUpString(word); const cleanedSuggestions = suggestions.map((_suggestion) => { const cleanedMeaning = cleanUpString(_suggestion.meaning); const cleanedExample = cleanUpString(_suggestion.example); return { meaning: cleanedMeaning, example: cleanedExample, }; }); return { word: cleanedWord, suggestions: cleanedSuggestions, }; };


Essa operação usa nosso modelo DictionarySuggestions de domínio como um argumento e executa uma limpeza de string, o que significa remover espaços desnecessários, quebras de linha, tabulações e letras maiúsculas. É bastante simples, sem complexidades ocultas.


Uma coisa importante a observar é que, neste ponto, você não precisa se preocupar com a implementação da API. Como lembrete, o repositório sempre retorna dados no modelo de domínio! Não pode ser de outra forma porque isso quebraria o princípio da inversão de dependência.


E por enquanto, nossa camada de domínio não depende de nada definido fora dela.

Adaptador - Vamos Juntar Tudo Isso

Neste ponto, tudo deve estar implementado e pronto para injeção de dependência. Aqui está a implementação final do módulo rest:


 import { getRestRepository } from '@repository/rest/rest.repository'; import { getRestApi } from '@api/rest/rest.api'; import { getDictionarySuggestionsUseCase } from '@domain/rest/rest.use-cases'; import { axiosInstance } from '@shared/axios.instance'; const restApi = getRestApi(axiosInstance); const restRepository = getRestRepository(restApi); export const restModule = { getDictionarySuggestions: getDictionarySuggestionsUseCase(restRepository).execute, };


Isso mesmo! Passamos pelo processo de implementação dos princípios da Arquitetura Limpa sem estar vinculado a uma estrutura específica. Essa abordagem garante que nosso código seja adaptável, facilitando a troca de estruturas ou bibliotecas, se necessário.


Quando se trata de testes, verificar o repositório é uma ótima maneira de entender como os testes são implementados e organizados nessa arquitetura.


Com uma base sólida em Clean Architecture, você pode escrever testes abrangentes que abrangem vários cenários, tornando seu aplicativo mais robusto e confiável.


Conforme demonstrado, seguir os princípios da Arquitetura Limpa e separar preocupações leva a uma estrutura de aplicativo sustentável, escalável e testável.


Essa abordagem torna mais fácil adicionar novos recursos, refatorar código e trabalhar com uma equipe em um projeto, garantindo o sucesso de longo prazo de seu aplicativo.

Apresentação

No aplicativo de exemplo, o React é usado para a camada de apresentação. No diretório do adaptador, há um arquivo adicional chamado hooks.ts que lida com a interação com o módulo rest. O conteúdo deste arquivo é o seguinte:


 import { restModule } from '@adapter/rest/rest.module'; import { useAxios } from '@shared/hooks'; export const useDictionarySuggestions = () => { const { data, error, isLoading, mutate } = useAxios(restModule.getDictionarySuggestions); return { dictionarySuggestions: data, getDictionarySuggestions: mutate, dictionarySuggestionsError: error, isDictionarySuggestionsLoading: isLoading, }; };


Essa implementação torna incrivelmente fácil trabalhar com a camada de apresentação. Ao usar o gancho useDictionarySuggestions , a camada de apresentação não precisa se preocupar com o gerenciamento de mapeamentos de dados ou outras responsabilidades não relacionadas à sua função principal.


Essa separação de interesses ajuda a manter os princípios da Clean Architecture, levando a um código mais gerenciável e sustentável.

Qual é o próximo?

Em primeiro lugar, encorajo você a mergulhar no código do repositório GitHub fornecido e explorar sua estrutura.


O que mais você pode fazer? O céu é o limite! Tudo depende de suas necessidades específicas de design. Por exemplo, você pode considerar a implementação da camada de dados incorporando um armazenamento de dados (Redux, MobX ou até mesmo algo personalizado - não importa).


Como alternativa, você pode experimentar diferentes métodos de comunicação entre as camadas, como usar RxJS para lidar com a comunicação assíncrona com o back-end, que pode envolver votação, notificações por push ou soquetes (essencialmente, sendo preparado para qualquer fonte de dados).


Em essência, sinta-se à vontade para explorar e experimentar como quiser, desde que mantenha a arquitetura em camadas e siga o princípio da dependência inversa. Certifique-se sempre de que o domínio esteja no centro do seu design.


Ao fazer isso, você criará uma estrutura de aplicativo flexível e sustentável que pode se adaptar a vários cenários e requisitos.

Resumo

Neste artigo, nos aprofundamos no conceito de Clean Architecture dentro do contexto de um aplicativo de aprendizado de idiomas construído usando React.


Destacamos a importância de manter uma arquitetura em camadas e aderir ao princípio da dependência inversa, bem como os benefícios de separar interesses.


Uma vantagem significativa da Clean Architecture é sua capacidade de permitir que você se concentre no aspecto de engenharia de seu aplicativo sem estar vinculado a uma estrutura específica. Essa flexibilidade permite que você adapte seu aplicativo a vários cenários e requisitos.


No entanto, existem algumas desvantagens para esta abordagem. Em alguns casos, seguir um padrão de arquitetura estrito pode levar ao aumento do código clichê ou à complexidade adicional na estrutura do projeto.


Além disso, confiar menos na documentação pode ser um pró e um contra - embora permita mais liberdade e criatividade, também pode resultar em confusão ou falta de comunicação entre os membros da equipe.


Apesar desses desafios potenciais, a implementação da Clean Architecture pode ser altamente benéfica, especialmente no contexto do React, onde não há um padrão de arquitetura universalmente aceito.


É essencial considerar sua arquitetura no início de um projeto, em vez de abordá-la após anos de luta.


Para explorar um exemplo da vida real de Clean Architecture em ação, sinta-se à vontade para conferir meu repositório em https://github.com/Levofron/NotionLingo . Você também pode se conectar comigo nas mídias sociais através dos links fornecidos no meu perfil.


Uau, este é provavelmente o artigo mais longo que já escrevi. É incrível!