LogoCésar Alberca
- 7 minutos

Cómo Llevo 5 Años Abstrayendo el Acceso a Datos

Diseña Mejor Arquitectura Frontend
Patrones de Arquitectura Frontend complejos explicados de forma sencilla. Envíos recurrentes y directos a tu correo. Conviértete en un mejor Frontend Architect.
Respeto tu privacidad. Cancela en cualquier momento.

La newsletter está escrita en inglés.

El patrón repository abstrae cómo una parte del código interactúa con los datos. ¿Estás haciendo llamadas a la API? ¿Accediendo a la base de datos? ¿Leyendo desde localStorage? ¡Pues eso podría ser un repositorio!

El Patrón Repository abstrae cómo interactuamos con datos que provienen de sistemas externos.

En el ecosistema frontend, solemos ver las llamadas al API hechas directamente en los componentes. Para mí, esto sacrifica mucha flexibilidad, dando como resultado sistemas más difíciles de escalar. ¿Por qué? Porque el acceso a los datos acopla tu sistema con terceros.

Aunque tengas control total sobre tu backend, yo seguiría considerándolo un tercero. Si tratas tu backend como un tercero, puedes hacer tus sistemas menos acoplados.

Si tu aplicación trata sobre viajes intergalácticos, aunque la forma de acceder a los datos es propensa a cambiar (la API podría cambiar la , cambiar la firma del objeto, etc.) el concepto de que necesitas obtener una lista de viajes para mostrarle al usuario no va a cambiar tanto.

Y para eso sirven los repositorios.

#Definiendo el Contrato del Repositorio

Siguiendo con la aplicación de viajes intergalácticos, veamos cómo podríamos empezar a definir un repositorio. Creemos el DestinationRepository:

export interface DestinationRepository { findAll(): Promise<Destination[]> }

¿Una interfaz? ¡Sí! Nos ayuda a hacer nuestro código más robusto, dado que cuando tratamos con abstracciones en lugar de concreciones, nuestro código tiende a ser más flexible.

Cuando creamos una interfaz, significa que necesitamos implementarla en algún lugar.

#Implementando el Repositorio

Para implementar el repositorio, hago algo diferente. Creo la interfaz del repositorio en una capa (que básicamente significa una carpeta) y la implementación del repositorio en una capa diferente (otra carpeta). ¿Por qué me complico tanto?

Bueno, a eso lo llamamos una Arquitectura de Puertos y AdaptadoresSe abre en una nueva pestaña, que hace nuestro código más robusto. Este tipo de arquitectura no es el punto principal de este post, así que hablaremos de ella en más detalle en otro new Date().day.

Implementemos ahora el DestinationApiRepository:

export class DestinationApiRepository implements DestinationRepository { async findAll(): Promise<Destination[]> { const response = await fetch('/api/destinations') if (!response.ok) { throw new GetDestinationsError() } const data = (await response.json()) as Destination[] return data } }

Por ahora podría parecer sobreingeniería, ¿verdad? Aquí tienes una lista de preguntas que podrías tener:

  • ¿Por qué no debería llamar directamente a la API desde mi componente React?
  • ¿Cuál es la diferencia entre un repositorio y un servicio?
  • ¿Por qué tener una interfaz cuando podría simplemente renombrar DestinationApiRepository a DestinationRepository?

Respondamos estas preguntas una por una.

#Preguntas

¿Por qué no debería llamar directamente a la API desde mi componente React? ¡Por S.O.L.I.D.Se abre en una nueva pestaña, claro está! Los componentes se encargan de renderizar, por lo que deberían tener lógica de renderizado. Los repositorios se encargan de acceder a los datos. Si mezclamos ambos, nos costará más cambiar el código. Esta separación hace que nuestro código sea más fácil de razonar y más fácil de cambiar.

¿Cuál es la diferencia entre un repositorio y un servicio? Los servicios son... bueno, ¡tienden a ser de todo! Y eso es un problema, lo llamamos todo servicio. Prefiero encontrar nombres que reflejen mejor la responsabilidad de cada parte de mi código. Voy incluso más allá e introduzco el concepto de casos de uso, del que he hablado en números anteriores de mi Newsletter de Arquitectura Frontend.

¿Por qué tener una interfaz cuando podría simplemente renombrar DestinationApiRepository a DestinationRepository? ¡Pues bien, podemos tener múltiples implementaciones de un repositorio! Para mí, eso es extremadamente útil. ¿Has necesitado alguna vez trabajar con datos de una API Rest que aún no estaba terminada? Pues puedes implementar un DestinationInMemoryRepository:

export class DestinationInMemoryRepository implements DestinationRepository { data = [DestinationMother.europe()] async findAll(): Promise<Destination[]> { return this.data } async create(createDestination: CreateDestination): Promise<void> { this.data.push({ ...createDestination, id: (Math.random() * 1000).toString() }) } }

¿Te preguntas qué es DestinationMother? Es un patrón de diseño de Martin FowlerSe abre en una nueva pestaña donde gestionas tus datos falsos usando Mothers, dando nombres significativos a los datos que podrías usar en las pruebas. Puedes leer más sobre el patrón de diseño Mother en este blogpost.

Ahora podemos cambiar las dependencias en tiempo de compilación:

// Antes // export const destinationApiRepository = new DestinationApiRepository() // Después export const destinationInMemoryRepository = new DestinationInMemoryRepository() export const getDestinationsQry = new GetDestinationsQry(destinationInMemoryRepository, destinationOrderer)

O durante tiempo de ejecución:

export const destinationApiRepository = new DestinationApiRepository() export const destinationInMemoryRepository = new DestinationInMemoryRepository() export const getDestinationsQry = new GetDestinationsQry(process.env.NODE_ENV === 'development' ? destinationInMemoryRepository : destinationApiRepository, destinationOrderer)

Lo que convierte esto en un patrón muy potente para gestionar escenarios donde podrías necesitar cambiar dinámicamente tu fuente de datos. Por ejemplo, si usas una API de pago durante el desarrollo, no querrás hacer peticiones a dicha API, puedes simplemente añadir un condicional para usar el repositorio en memoria.

#Conclusiones Adicionales

Habiendo llegado casi al final del post, permíteme compartir un consejo interesante. Puedes crear interfaces reutilizables para tener una forma unificada de llamar a tus repositorios:

export interface FindableAll<Result> { findAll(): Promise<Result[]> } export interface Creatable<Params, Result = void> { create(params: Params): Promise<Result> } export interface DestinationRepository extends FindableAll<Destination>, Creatable<CreateDestination> {}

¡Olvídate de recordar si llamas a las cosas create, upsert, save u otros nombres diferentes para gestionar la creación de entidades! Hacer cumplir las cosas con la compilación siempre será mejor que hacerlas cumplir porque podrían estar documentadas. El código es la documentación.

#Conclusión

El Patrón Repository trata con los datos. Más concretamente, qué podemos hacer con los datos (Interfaz del Repositorio) y cómo y dónde los obtenemos (Implementación del Repositorio).

Permite intercambiar tus repositorios en tiempo de compilación y de ejecución, encapsula tu código y proporciona consistencia.

Gracias por leer este post
¿Quieres apoyar ideas que merecen ser compartidas? Apoyar este blog me compra tiempo para escribir.
Más tiempo significa más valor para ti.