paint-brush
Probar una arquitectura limpia en una aplicación frontend: ¿tiene sentido?por@playerony
10,637 lecturas
10,637 lecturas

Probar una arquitectura limpia en una aplicación frontend: ¿tiene sentido?

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

Demasiado Largo; Para Leer

Los desarrolladores frontend enfrentan el desafío de crear arquitecturas escalables y mantenibles. Es posible que muchas de las ideas arquitectónicas propuestas nunca se hayan implementado en entornos de producción del mundo real. Este artículo tiene como objetivo proporcionar a los desarrolladores frontend las herramientas que necesitan para navegar en el mundo en constante evolución del desarrollo de sitios web.
featured image - Probar una arquitectura limpia en una aplicación frontend: ¿tiene sentido?
Paweł Wojtasiński HackerNoon profile picture

A medida que evoluciona el panorama digital, también lo hace la complejidad de los sitios web modernos. Con una demanda cada vez mayor de una mejor experiencia de usuario y funciones avanzadas, los desarrolladores frontend enfrentan el desafío de crear arquitecturas escalables, fáciles de mantener y eficientes.


Entre la plétora de artículos y recursos disponibles sobre arquitectura frontend, un número significativo se centra en la arquitectura limpia y su adaptación. De hecho, más del 50 % de los casi 70 artículos encuestados analizan la arquitectura limpia en el contexto del desarrollo front-end.


A pesar de la gran cantidad de información, persiste un problema evidente: es posible que muchas de las ideas arquitectónicas propuestas nunca se hayan implementado en entornos de producción del mundo real. Esto plantea dudas sobre su eficacia y aplicabilidad en escenarios prácticos.


Impulsado por esta preocupación, me embarqué en un viaje de seis meses para implementar Arquitectura Limpia en la interfaz, permitiéndome confrontar las realidades de estas ideas y separar el trigo de la paja.


En este artículo, compartiré mis experiencias y conocimientos de este viaje, ofreciendo una guía completa sobre cómo implementar con éxito la arquitectura limpia en la interfaz.


Al arrojar luz sobre los desafíos, las mejores prácticas y las soluciones del mundo real, este artículo tiene como objetivo proporcionar a los desarrolladores frontend las herramientas que necesitan para navegar en el mundo en constante evolución del desarrollo de sitios web.

Marcos

En el ecosistema digital de rápida evolución actual, los desarrolladores tienen muchas opciones cuando se trata de marcos frontend. Esta abundancia de opciones soluciona numerosos problemas y simplifica el proceso de desarrollo.


Sin embargo, también genera debates interminables entre los desarrolladores, cada uno de los cuales afirma que su marco preferido es superior a los demás. La verdad es que, en nuestro mundo acelerado, surgen nuevas bibliotecas de JavaScript a diario y los marcos se introducen casi mensualmente.


Para mantener la flexibilidad y la adaptabilidad en un entorno tan dinámico, necesitamos una arquitectura que trascienda marcos y tecnologías específicos.


Esto es particularmente crucial para las empresas de productos o los contratos a largo plazo que involucran mantenimiento, donde se deben acomodar las tendencias cambiantes y los avances tecnológicos.


Ser independientes de los detalles, como los marcos, nos permite centrarnos en el producto en el que estamos trabajando y prepararnos para los cambios que puedan surgir durante su ciclo de vida.


No temáis; este artículo pretende dar respuesta a este dilema.

Cooperación de equipo completo

En mi búsqueda para implementar la arquitectura limpia en la interfaz, trabajé en estrecha colaboración con varios desarrolladores fullstack y backend para garantizar que la arquitectura fuera comprensible y mantenible, incluso para aquellos con una experiencia mínima en la interfaz.


Por lo tanto, uno de los requisitos principales de nuestra arquitectura es su accesibilidad para los desarrolladores de back-end que pueden no estar bien versados en las complejidades de front-end, así como para los desarrolladores fullstack que pueden no tener una amplia experiencia en front-end.


Al fomentar la cooperación fluida entre los equipos de front-end y back-end, la arquitectura tiene como objetivo cerrar la brecha y crear una experiencia de desarrollo unificada.

Fundamentos teóricos

Desafortunadamente, para construir algunas cosas increíbles, necesitamos obtener algunos conocimientos básicos. Una comprensión clara de los principios subyacentes no solo facilitará el proceso de implementación, sino que también garantizará que la arquitectura se adhiera a las mejores prácticas en el desarrollo de software.


En esta sección, presentaremos tres conceptos clave que forman la base de nuestro enfoque arquitectónico: principios SOLID , arquitectura limpia (que en realidad proviene de principios SOLID) y diseño atómico . Si está convencido de estas áreas, puede omitir esta sección.

Principios SOLIDOS

SOLID es un acrónimo que representa cinco principios de diseño que guían a los desarrolladores en la creación de software escalable, mantenible y modular:


  • Principio de responsabilidad única (SRP) : este principio establece que una clase debe tener solo una razón para cambiar, lo que significa que debe tener una responsabilidad única. Al adherirse a SRP, los desarrolladores pueden crear un código más enfocado, mantenible y comprobable.


  • Principio abierto/cerrado (OCP) : según OCP, las entidades de software deben estar abiertas para la extensión pero cerradas para la modificación. Esto significa que los desarrolladores deberían poder agregar nuevas funciones sin alterar el código existente, lo que reduce el riesgo de introducir errores.


  • Principio de sustitución de Liskov (LSP) : LSP afirma que los objetos de una clase derivada deberían poder reemplazar objetos de la clase base sin afectar la corrección del programa. Este principio promueve el uso adecuado de la herencia y el polimorfismo.


  • Principio de segregación de interfaz (ISP) : ISP enfatiza que los clientes no deben verse obligados a depender de interfaces que no utilizan. Al crear interfaces más pequeñas y enfocadas, los desarrolladores pueden garantizar una mejor organización y mantenimiento del código.


  • Principio de inversión de dependencia (DIP) : DIP alienta a los desarrolladores a depender de abstracciones en lugar de implementaciones concretas. Este principio fomenta una base de código más modular, comprobable y flexible.


Si desea explorar este tema con más profundidad, cosa que le recomiendo encarecidamente que haga, no hay problema. Sin embargo, por ahora, lo que he presentado es suficiente para ir más allá.


¿Y qué nos aporta SOLID en cuanto a este artículo?

Arquitectura limpia

Robert C. Martin, basado en los principios SOLID y su amplia experiencia en el desarrollo de diversas aplicaciones, propuso el concepto de Arquitectura Limpia. Cuando se habla de este concepto, a menudo se hace referencia al siguiente diagrama para representar visualmente su estructura:



Entonces, la Arquitectura Limpia no es un concepto nuevo; ha sido ampliamente utilizado en varios paradigmas de programación, incluida la programación funcional y el desarrollo de back-end.


Bibliotecas como Lodash y numerosos marcos de back-end han adoptado este enfoque arquitectónico, que se basa en los principios SOLID.


Clean Architecture enfatiza la separación de preocupaciones y la creación de capas independientes y comprobables dentro de una aplicación, con el objetivo principal de hacer que el sistema sea fácil de entender, mantener y modificar.


La arquitectura se organiza en círculos concéntricos o capas; cada uno con límites claros, dependencias y responsabilidades:


  • Entidades : estos son los objetos comerciales y las reglas principales dentro de la aplicación. Las entidades suelen ser objetos sencillos que representan los conceptos esenciales o las estructuras de datos del dominio, como usuarios, productos o pedidos.


  • Casos de uso : también conocidos como Interactores, los Casos de uso definen las reglas comerciales específicas de la aplicación y organizan la interacción entre las Entidades y los sistemas externos. Los casos de uso son responsables de implementar la funcionalidad central de la aplicación y deben ser independientes de las capas externas.


  • Adaptadores de interfaz : estos componentes actúan como un puente entre las capas interna y externa, convirtiendo datos entre el caso de uso y los formatos del sistema externo. Los adaptadores de interfaz incluyen repositorios, presentadores y controladores, que permiten que la aplicación interactúe con bases de datos, API externas y marcos de interfaz de usuario.


  • Marcos y controladores : esta capa más externa comprende los sistemas externos, como bases de datos, marcos de interfaz de usuario y bibliotecas de terceros. Frameworks y Drivers son responsables de proporcionar la infraestructura necesaria para ejecutar la aplicación e implementar las interfaces definidas en las capas internas.


Clean Architecture promueve el flujo de dependencias desde las capas externas a las capas internas, lo que garantiza que la lógica comercial central permanezca independiente de las tecnologías o marcos específicos utilizados.


Esto da como resultado una base de código flexible, mantenible y comprobable que puede adaptarse fácilmente a los requisitos cambiantes o pilas de tecnología.

Diseño atómico

Atomic Design es una metodología que organiza los componentes de la interfaz de usuario al dividir las interfaces en sus elementos más básicos y luego volver a ensamblarlos en estructuras más complejas. Brad Frost introdujo el concepto por primera vez en 2008 en un artículo titulado "Metodología de diseño atómico".


Aquí hay un gráfico que muestra el concepto de Diseño Atómico:



Consta de cinco niveles distintos:


  • Átomos : las unidades más pequeñas e indivisibles de la interfaz, como botones, entradas y etiquetas.


  • Moléculas : grupos de átomos que funcionan juntos, formando componentes de interfaz de usuario más complejos, como formularios o barras de navegación.


  • Organismos : combinaciones de moléculas y átomos que crean distintas secciones de la interfaz, como encabezados o pies de página.


  • Plantillas : representan el diseño y la estructura de una página, proporcionando un esqueleto para la colocación de organismos, moléculas y átomos.


  • Páginas : Instancias de plantillas rellenas con contenido real, mostrando la interfaz final.


Al adoptar el diseño atómico, los desarrolladores pueden obtener varios beneficios, como la modularidad, la reutilización y una estructura clara para los componentes de la interfaz de usuario, ya que requiere que sigamos el enfoque del sistema de diseño, pero este no es el tema de este artículo, así que siga adelante.

Estudio de caso: NotionLingo

Con el fin de desarrollar una perspectiva bien informada sobre la arquitectura limpia para el desarrollo frontend, me embarqué en un viaje para crear una aplicación. Durante un período de seis meses, obtuve información valiosa y experiencia mientras trabajaba en este proyecto.


En consecuencia, los ejemplos proporcionados a lo largo de este artículo se basan en mi experiencia práctica con la aplicación. Para mantener la transparencia, todos los ejemplos se derivan de un código de acceso público.


Puede explorar el resultado final visitando el repositorio en https://github.com/Levofron/NotionLingo .

Implementación de arquitectura limpia

Como se mencionó anteriormente, existen numerosas implementaciones de Clean Architecture disponibles en línea. Sin embargo, se pueden identificar algunos elementos comunes en estas implementaciones:


  • Capa de dominio : el núcleo de nuestra aplicación, que abarca modelos relacionados con el negocio, casos de uso y operaciones.


  • Capa API : Responsable de interactuar con las API del navegador.


  • Capa de repositorio : Sirve como un puente entre el dominio y las capas de API, proporcionando un espacio para asignar tipos de API a nuestros tipos de dominio.


  • Capa de interfaz de usuario : Acomoda nuestros componentes, formando la interfaz de usuario.


Al comprender estos puntos en común, podemos apreciar la estructura fundamental de la Arquitectura Limpia y adaptarla a nuestras necesidades específicas.

Dominio

La parte central de nuestra aplicación contiene:


  • Casos de uso : los casos de uso describen las reglas comerciales para varias operaciones, como guardar, actualizar y obtener datos. Por ejemplo, un caso de uso podría implicar obtener una lista de palabras de Notion o aumentar la racha diaria del usuario de palabras aprendidas.


    Esencialmente, los casos de uso manejan las tareas y procesos de la aplicación desde una perspectiva comercial, asegurando que el sistema funcione de acuerdo con los objetivos deseados.


  • Modelos : los modelos representan las entidades comerciales dentro de la aplicación. Estos se pueden definir mediante interfaces de TypeScript, lo que garantiza que se alineen con las necesidades y los requisitos comerciales.


    Por ejemplo, si un caso de uso implica obtener una lista de palabras de Notion, necesitaría un modelo para describir con precisión la estructura de datos de esa lista, adhiriéndose a las reglas y restricciones comerciales apropiadas.


  • Operaciones : a veces, definir ciertas tareas como casos de uso puede no ser factible, o es posible que desee crear funciones reutilizables que se puedan emplear en varias partes de su dominio. Por ejemplo, si necesita escribir una función para buscar una palabra de noción por nombre, aquí es donde deben residir dichas operaciones.


    Las operaciones son útiles para encapsular la lógica específica del dominio que se puede compartir y utilizar en varios contextos dentro de la aplicación.


  • Interfaces de repositorio : los casos de uso requieren un medio para acceder a los datos. De acuerdo con el Principio de Inversión de Dependencia, la capa de dominio no debe depender de ninguna otra capa (mientras que las otras capas dependen de ella); por lo tanto, esta capa define las interfaces para los repositorios.


    Es importante tener en cuenta que especifica las interfaces, no los detalles de implementación. Los propios repositorios utilizan el patrón de repositorio, que es independiente de la fuente de datos real y enfatiza la lógica para obtener o enviar datos hacia y desde esas fuentes.


    Es crucial mencionar que un solo repositorio puede implementar varias API y un solo caso de uso puede utilizar varios repositorios.

API

Esta capa es responsable del acceso a los datos y puede comunicarse con varias fuentes según sea necesario. Teniendo en cuenta que estamos desarrollando una aplicación frontend, esta capa servirá principalmente como contenedor para las API del navegador.


Esto incluye API para REST, almacenamiento local, IndexedDB, síntesis de voz y más.


Es importante tener en cuenta que si desea generar tipos OpenAPI y clientes HTTP, la capa API es el lugar ideal para colocarlos. Dentro de esta capa tenemos:


  • Adaptador de API : el adaptador de API es un adaptador especializado para las API de navegador utilizadas en nuestra aplicación. Este componente gestiona las llamadas REST y la comunicación con la memoria de la aplicación o cualquier otra fuente de datos que desee utilizar.


    Incluso puede crear e implementar su propio sistema de almacenamiento de objetos si lo desea. Al tener un adaptador de API dedicado, puede mantener una interfaz coherente para interactuar con varias fuentes de datos, lo que facilita su actualización o cambio según sea necesario.


  • Tipos : este es un lugar para todos los tipos relacionados con su API. Estos tipos no están directamente relacionados con el dominio, pero sirven como descripciones de las respuestas sin procesar recibidas de la API. En la siguiente capa, estos tipos serán esenciales para un mapeo y procesamiento adecuados.

Repositorio

La capa de repositorio juega un papel crucial en la arquitectura de la aplicación al administrar la integración de múltiples API, mapear tipos específicos de API a tipos de dominio e incorporar operaciones para transformar datos.


Si desea combinar la API de síntesis de voz con el almacenamiento local, por ejemplo, este es el lugar perfecto para hacerlo. Esta capa contiene:


  • Implementación de repositorio : Son implementaciones concretas de las interfaces declaradas en la capa de dominio. Son capaces de trabajar con múltiples fuentes de datos, asegurando flexibilidad y adaptabilidad dentro de la aplicación.


  • Operaciones : se pueden denominar mapeadores, transformadores o ayudantes. En este contexto, operaciones es un término adecuado. Este directorio contiene todas las funciones responsables de mapear las respuestas API sin procesar a sus tipos de dominio correspondientes, lo que garantiza que los datos estén estructurados correctamente para su uso dentro de la aplicación.

Adaptador


La capa del adaptador es responsable de orquestar las interacciones entre estas capas y unirlas. Esta capa solo contiene módulos responsables de:


  • Inyección de dependencia : la capa del adaptador administra las dependencias entre las capas de la API, el repositorio y el dominio. Al manejar la inyección de dependencia, la capa del adaptador garantiza una separación clara de las preocupaciones y promueve la reutilización eficiente del código.


  • Organización del módulo : la capa del adaptador organiza la aplicación en módulos según sus funcionalidades (p. ej., almacenamiento local, REST, síntesis de voz, Supabase). Cada módulo encapsula una funcionalidad específica, proporcionando una estructura limpia y modular para la aplicación.


  • Creación de acciones : la capa de adaptador crea acciones mediante la combinación de casos de uso de la capa de dominio con los repositorios apropiados. Estas acciones sirven como puntos de entrada para que la aplicación interactúe con las capas subyacentes.

Presentación

La capa de presentación está a cargo de representar la interfaz de usuario (UI) y manejar las interacciones del usuario con la aplicación. Aprovecha el adaptador, el dominio y las capas compartidas para crear una interfaz de usuario funcional e interactiva.


La capa de presentación emplea la metodología de diseño atómico para organizar sus componentes, lo que da como resultado una aplicación escalable y mantenible. Sin embargo, esta capa no será el enfoque principal de este artículo, ya que no es el tema principal en términos de implementación de Arquitectura Limpia.

Compartido

Es necesario un lugar designado para todos los elementos comunes, como utilidades centralizadas, configuraciones y lógica compartida. Sin embargo, no profundizaremos demasiado en esta capa en este artículo.


Vale la pena mencionarlo solo para comprender cómo se administran y comparten los componentes comunes en toda la aplicación.

Estrategias de prueba para cada capa

Ahora, antes de sumergirse en la codificación, es esencial hablar sobre las pruebas. Asegurar la confiabilidad y corrección de su aplicación es vital, y es crucial implementar una estrategia de prueba sólida para cada capa de la arquitectura.


  • Capa de dominio : la prueba unitaria es el método principal para probar la capa de dominio. Concéntrese en probar los modelos de dominio, las reglas de validación y la lógica comercial, asegurándose de que se comporten correctamente en diversas condiciones. Adopte el desarrollo basado en pruebas (TDD) para impulsar el diseño de sus modelos de dominio y confirmar que su lógica empresarial es sólida.


  • Capa API : pruebe la capa API mediante pruebas de integración. Estas pruebas deben centrarse en garantizar que la API interactúe correctamente con los servicios externos y que las respuestas tengan el formato adecuado. Utilice herramientas como marcos de prueba automatizados, como Jest, para simular llamadas API y validar las respuestas.


  • Capa de repositorio : para la capa de repositorio, puede usar una combinación de pruebas unitarias y de integración. Las pruebas unitarias se pueden usar para probar métodos de repositorios individuales, mientras que las pruebas de integración deben centrarse en verificar que los repositorios interactúen correctamente con sus API.


  • Capa de adaptador : las pruebas unitarias son adecuadas para probar la capa de adaptador. Estas pruebas deben garantizar que los adaptadores inyecten dependencias correctamente y gestionen las transformaciones de datos entre capas. Simular las dependencias, como la API o las capas del repositorio, puede ayudar a aislar la capa del adaptador durante las pruebas.


Al implementar una estrategia de prueba integral para cada capa de la arquitectura, puede garantizar la confiabilidad, la corrección y la capacidad de mantenimiento de su aplicación al tiempo que reduce la probabilidad de introducir errores durante el desarrollo.


Sin embargo, si está creando una aplicación pequeña, las pruebas de integración en la capa del adaptador deberían ser suficientes.

Codifiquemos algo

Muy bien, ahora que tiene una sólida comprensión de la arquitectura limpia y tal vez incluso haya formado su propia opinión al respecto, profundicemos un poco más y exploremos un código real.


Tenga en cuenta que solo presentaré un ejemplo simple aquí; sin embargo, si está interesado en ejemplos más detallados, siéntase libre de explorar mi repositorio de GitHub mencionado al principio de este artículo.


En la "vida real", la arquitectura limpia realmente brilla en aplicaciones grandes de nivel empresarial, mientras que podría ser una exageración para proyectos más pequeños. Dicho esto, vayamos al grano.


Usando mi aplicación como ejemplo, demostraré cómo realizar una llamada a la API para obtener sugerencias del diccionario para una palabra determinada. Este punto final de API en particular recupera una lista de significados y ejemplos mediante el raspado web de dos sitios web.


Desde una perspectiva comercial, este punto final es crucial para la vista "Buscar palabra", que permite a los usuarios buscar una palabra específica. Una vez que el usuario encuentra la palabra e inicia sesión, puede agregar la información extraída de la web a su base de datos de Notion.

Estructura de carpetas

Para comenzar, debemos establecer una estructura de carpetas que refleje con precisión las capas que discutimos anteriormente. La estructura debe parecerse a lo siguiente:


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


El directorio del cliente tiene un propósito similar al de la carpeta "src" en muchos proyectos. En este proyecto específico de Next.js, adopté la convención de nombrar la carpeta de frontend como "cliente" y la carpeta de backend como "servidor".


Este enfoque permite una clara distinción entre los dos componentes principales de la aplicación.

Subdirectorios

Elegir la estructura de carpetas adecuada para su proyecto es, de hecho, una decisión crucial que debe tomarse al principio del proceso de desarrollo. Los diferentes desarrolladores tienen sus propias preferencias y enfoques cuando se trata de organizar los recursos.


Algunos pueden agrupar recursos por nombres de página, otros pueden seguir las convenciones de nomenclatura de subdirectorios generadas por OpenAPI y, aún así, otros pueden creer que su aplicación es demasiado pequeña para garantizar cualquiera de esas soluciones.


La clave es elegir la estructura que mejor se adapte a las necesidades específicas y la escala de su proyecto, manteniendo una organización de recursos clara y fácil de mantener.


Estoy en el tercer grupo, por lo que mi estructura se ve así:


 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


Decidí omitir las capas compartidas y de presentación en este artículo, ya que creo que aquellos que quieran profundizar más pueden consultar mi repositorio para obtener más información. Ahora, procedamos con algunos ejemplos de código para ilustrar cómo se puede aplicar la arquitectura limpia en una aplicación frontend.

Definición de dominio

Consideremos nuestros requisitos. Como usuario, me gustaría recibir una lista de sugerencias, incluidos sus significados y ejemplos. Por lo tanto, una sola sugerencia de diccionario se puede modelar de la siguiente manera:


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


Ahora que hemos descrito una sola sugerencia de diccionario, es importante mencionar que, a veces, la palabra obtenida a través del web scraping difiere o se corrige en comparación con lo que escribió el usuario. Para acomodar esto, usaremos la versión corregida más adelante en nuestra aplicación.


En consecuencia, necesitamos definir una interfaz que incluya una lista de sugerencias de diccionario y correcciones de palabras. La interfaz final se ve así:


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


Estamos exportando esta interfaz, por lo que se incluye la palabra clave export .

Interfaz de repositorio

Tenemos nuestro modelo, y ahora es el momento de ponerlo en práctica.


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


En este punto, todo debería estar claro. ¡Es importante tener en cuenta que no estamos discutiendo la API en absoluto aquí! La estructura del repositorio en sí es bastante simple: solo un objeto con algunos métodos, donde cada método devuelve datos de un tipo específico de forma asíncrona.


Tenga en cuenta que el repositorio siempre devuelve datos en el formato del modelo de dominio.

Caso de uso

Ahora, definamos nuestra regla de negocio como un caso de uso. El código se ve así:


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


Lo primero que se debe tener en cuenta es la lista de tipos comunes utilizados para definir casos de uso. Para lograr esto, creé un archivo use-cases.types.ts en el directorio del dominio:


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


Esto me permite compartir fácilmente tipos para casos de uso entre mis subdirectorios. La definición de UseCaseWithSingleParamAndPromiseResult tiene este aspecto:


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


Este enfoque ayuda a mantener la coherencia y la reutilización de los tipos de casos de uso en toda la capa de dominio.


Quizás se pregunte por qué necesitamos la función execute . Aquí, tenemos una fábrica que devuelve el caso de uso real.


Esta elección de diseño se debe al hecho de que no queremos hacer referencia a la implementación del repositorio directamente en el código del caso de uso, ni queremos que el repositorio sea utilizado por una importación. Este enfoque nos permite aplicar fácilmente la inyección de dependencia más adelante.


Al usar el patrón de fábrica y la función execute , podemos mantener los detalles de implementación del repositorio separados del código del caso de uso, lo que mejora la modularidad y la capacidad de mantenimiento de la aplicación.


Este enfoque sigue el principio de inversión de dependencia, donde la capa de dominio no depende de ninguna otra capa y permite una mayor flexibilidad cuando se trata de intercambiar diferentes implementaciones de repositorio o modificar la arquitectura de la aplicación.

Definición de API

Primero, definamos nuestra interfaz:


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


Como puede ver, la definición de esta función en la interfaz se parece mucho a la del repositorio. Dado que el tipo de dominio ya describe la respuesta, no es necesario volver a crear el mismo tipo.


Es importante tener en cuenta que nuestra API devuelve datos sin procesar, por lo que devolvemos la AxiosResponse<DictionarySuggestions> completa. Al hacerlo, mantenemos una clara separación entre la API y las capas de dominio, lo que permite una mayor flexibilidad en el manejo y la transformación de datos.


La implementación de esta API se ve así:


 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; } });


En este punto, las cosas se ponen más interesantes. El primer aspecto importante a discutir es la inyección de nuestra axiosInstance . Esto hace que nuestro código sea muy flexible y nos permite construir pruebas sólidas fácilmente. Este es también el lugar donde manejamos la codificación o el análisis de los parámetros de consulta.


Sin embargo, también puede realizar otras acciones aquí, como recortar la cadena de entrada. Al inyectar axiosInstance , mantenemos una clara separación de preocupaciones y nos aseguramos de que la implementación de la API se adapte a diferentes escenarios o cambios en los servicios externos.

Implementación del repositorio

Como nuestra interfaz ya está definida por el dominio, todo lo que tenemos que hacer es implementar nuestro repositorio. Entonces, la implementación final se ve así:

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


Un aspecto importante a mencionar está relacionado con las API. Nuestro getRestRepository nos permite pasar un restApi previamente definido. Esto es ventajoso porque, como se mencionó anteriormente, permite una prueba más fácil. 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, }; };


Esta operación toma nuestro modelo DictionarySuggestions de dominio como argumento y realiza una limpieza de cadenas, lo que significa eliminar espacios innecesarios, saltos de línea, tabulaciones y mayúsculas. Es bastante sencillo, sin complejidades ocultas.


Una cosa importante a tener en cuenta es que en este punto es que no necesita preocuparse por la implementación de su API. Como recordatorio, ¡el repositorio siempre devuelve datos en el modelo de dominio! No puede ser de otra manera porque hacerlo rompería el principio de inversión de dependencia.


Y por ahora, nuestra capa de dominio no depende de nada definido fuera de ella.

Adaptador - Pongamos todo esto junto

En este punto, todo debería estar implementado y listo para la inyección de dependencia. Aquí está la implementación final del módulo de descanso:


 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, };


¡Así es! Hemos pasado por el proceso de implementar los principios de la arquitectura limpia sin estar atados a un marco específico. Este enfoque garantiza que nuestro código sea adaptable, lo que facilita el cambio de marcos o bibliotecas si es necesario.


Cuando se trata de pruebas, consultar el repositorio es una excelente manera de comprender cómo se implementan y organizan las pruebas en esta arquitectura.


Con una base sólida en arquitectura limpia, puede escribir pruebas integrales que cubran varios escenarios, lo que hace que su aplicación sea más sólida y confiable.


Como se demostró, seguir los principios de la arquitectura limpia y separar las preocupaciones conduce a una estructura de aplicación mantenible, escalable y comprobable.


En última instancia, este enfoque facilita la adición de nuevas funciones, la refactorización del código y el trabajo con un equipo en un proyecto, lo que garantiza el éxito a largo plazo de su aplicación.

Presentación

En la aplicación de ejemplo, React se usa para la capa de presentación. En el directorio del adaptador, hay un archivo adicional llamado hooks.ts que maneja la interacción con el resto del módulo. El contenido de este archivo es el siguiente:


 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, }; };


Esta implementación hace que sea increíblemente fácil trabajar con la capa de presentación. Al usar el enlace useDictionarySuggestions , la capa de presentación no tiene que preocuparse por administrar asignaciones de datos u otras responsabilidades que no están relacionadas con su función principal.


Esta separación de preocupaciones ayuda a mantener los principios de la arquitectura limpia, lo que lleva a un código más manejable y mantenible.

¿Que sigue?

En primer lugar, lo animo a sumergirse en el código del repositorio de GitHub proporcionado y explorar su estructura.


¿Qué más puedes hacer? ¡El cielo es el límite! Todo depende de sus necesidades específicas de diseño. Por ejemplo, podría considerar implementar la capa de datos incorporando un almacén de datos (Redux, MobX o incluso algo personalizado, no importa).


Alternativamente, puede experimentar con diferentes métodos de comunicación entre las capas, como usar RxJS para manejar la comunicación asincrónica con el backend, lo que podría implicar sondeos, notificaciones push o sockets (esencialmente, estar preparado para cualquier fuente de datos).


En esencia, siéntete libre de explorar y experimentar como quieras, siempre y cuando mantengas la arquitectura en capas y cumplas con el principio de dependencia inversa. Asegúrese siempre de que el dominio esté en el centro de su diseño.


Al hacerlo, creará una estructura de aplicación flexible y mantenible que puede adaptarse a varios escenarios y requisitos.

Resumen

En este artículo, profundizamos en el concepto de arquitectura limpia en el contexto de una aplicación de aprendizaje de idiomas creada con React.


Destacamos la importancia de mantener una arquitectura en capas y adherirse al principio de dependencia inversa, así como los beneficios de separar las preocupaciones.


Una ventaja significativa de Clean Architecture es su capacidad para permitirle concentrarse en el aspecto de ingeniería de su aplicación sin estar atado a un marco específico. Esta flexibilidad le permite adaptar su aplicación a varios escenarios y requisitos.


Sin embargo, hay algunos inconvenientes en este enfoque. En algunos casos, seguir un patrón arquitectónico estricto puede conducir a un aumento del código repetitivo o a una mayor complejidad en la estructura del proyecto.


Además, depender menos de la documentación puede ser tanto una ventaja como una desventaja: si bien permite una mayor libertad y creatividad, también puede generar confusión o falta de comunicación entre los miembros del equipo.


A pesar de estos desafíos potenciales, implementar una arquitectura limpia puede ser muy beneficioso, particularmente en el contexto de React, donde no existe un patrón arquitectónico universalmente aceptado.


Es esencial considerar su arquitectura al comienzo de un proyecto en lugar de abordarlo después de años de lucha.


Para explorar un ejemplo de la vida real de la arquitectura limpia en acción, no dude en consultar mi repositorio en https://github.com/Levofron/NotionLingo . También puede conectarse conmigo en las redes sociales a través de los enlaces proporcionados en mi perfil.


Wow, este es probablemente el artículo más largo que he escrito. ¡Se siente increíble!