Inicio / Artículos / Por qué las abstracciones del frontend se convierten silenciosamente en deuda técnica

Por qué las abstracciones del frontend se convierten silenciosamente en deuda técnica

Aprenda por qué las abstracciones prematuras en el frontend añaden complejidad oculta y cómo determinar cuándo realmente vale la pena crear componentes compartidos, hooks o utilidades.

2389 palabras

En casi todos los repositorios de código frontend llega un momento en que deja de ser una aplicación sencilla para convertirse en un framework diseñado para respaldar a una aplicación.

Se introduce una biblioteca de componentes.

Luego llega un sistema de diseño.

A continuación, se añade una capa de gestión de estado.

Después aparece una abstracción para la obtención de datos.

Luego un hook personalizado envuelve esa abstracción para la obtención de datos.

Finalmente, alguien escribe un componente de formulario genérico que acepta un objeto de configuración que describe cómo deben comportarse los formularios.

En poco tiempo, cambiar algo tan simple como un botón implica revisar seis archivos, tres capas de abstracción y una convención cuyo creador ya nadie puede identificar.

Lo curioso es que ninguna de estas decisiones individuales pareció irracional en su momento.

Ese es realmente el problema central en la forma en que los equipos frontend manejan la abstracción.

La mayoría de las abstracciones no son inherentemente malas, y muchas realmente ayudan. El problema es que los desarrolladores front-end se han vuelto excepcionalmente hábiles para crear abstracciones antes de haber reunido pruebas suficientes de que esas abstracciones sean realmente necesarias.

Ya no estamos simplemente abstractando la complejidad existente.

Estamos abstractando incluso la mera posibilidad de que surja complejidad algún día.

Esa costumbre genera un tipo específico de deuda técnica.

La abstracción suele comenzar con buenas intenciones

Imagínese tres componentes separados, cada uno encargado de obtener datos del usuario.

El primero tiene algo de lógica de carga duplicada.

El segundo repite casi el mismo patrón.

El tercero hace algo ligeramente diferente otra vez.

Alguien se da cuenta de la repetición y sugiere:

"Probablemente deberíamos integrar esto en una abstracción compartida."

Esa es una observación válida por sí sola.

Entonces el equipo crea un gancho personalizado:

const { data, loading, error } = useUserData(userId);

Bien organizado y limpio.

Luego, otro componente necesita un pequeño ajuste en su comportamiento.

En lugar de acceder directamente a la API, el equipo opta por otra alternativa:

useUserData(userId, {
  includePermissions: true,
  cache: true,
  retry: 3,
});

Unos meses después, el gancho ya no se limita a obtener registros de usuarios.

Ahora también gestiona el caché, las reintentos, las verificaciones de permisos, las transformaciones de datos, la normalización de errores, las actualizaciones optimistas y diversas peculiaridades específicas de cada funcionalidad.

La duplicación original ha desaparecido.

Pero algo más ha surgido: una brecha cada vez mayor entre el código y lo que realmente hace.

Observar un componente ya no te indica de dónde provienen sus datos.

Primero hay que comprender la abstracción que se encuentra frente a uno.

Ese es el compromiso del que nadie habla.

La abstracción no elimina la complejidad.

Solo la reubica.

A veces esa reubicación realmente vale la pena.

Otras veces simplemente se han intercambiado cinco líneas de lógica sencilla por trescientas líneas de mecanismos internos.

La abstracción tiene un costo

A los desarrolladores se les enseña desde temprano a considerar la duplicación como un problema.

Es comprensible, ya que a menudo lo es.

Pero la duplicación está lejos de ser el único tipo de complejidad.

Otras formas incluyen:

  • indirección
  • configuración
  • comportamiento implícito
  • API genéricas
  • dependencias ocultas
  • convenciones
  • herencia
  • componentes de envoltura
  • depuración que solo existe debido a la abstracción en sí
  • carga cognitiva

En algunos casos, la duplicación es realmente más económica que una abstracción compleja.

Compare dos enfoques.

Uno repite un pequeño fragmento de lógica tres veces.

El otro crea una herramienta genérica con una docena de parámetros, justificada porque tres casos de uso actuales coinciden aproximadamente en un 60 por ciento.

La segunda opción parece más pulida, más “diseñada”.

También puede resultar considerablemente más difícil de mantener.

Aquí es donde el trabajo en frontend suele cometer errores.

Los equipos se enfocan en el código DRY en lugar de en un código fácil de seguir.

Esos objetivos no son intercambiables.

El ecosistema frontend fomenta esto

El desarrollo de frontend tiene una relación peculiar con la abstracción, principalmente porque todo el ecosistema está estructurado en capas por diseño.

Una sola aplicación moderna puede incorporar fácilmente un framework de renderizado para crear componentes, algún tipo de biblioteca de enrutamiento, un gestor de estado del lado del cliente, una herramienta separada para manejar el estado del lado del servidor, además de una biblioteca de formularios acompañada de su propio paquete de validación. Sobre todo esto se encuentra un sistema de diseño, un conjunto de componentes UI preconstruidos, una abstracción de estilos encima del CSS puro, una herramienta de compilación que coordina todo y un framework de pruebas que supervisa toda la configuración.

Cada uno de estos elementos resuelve un problema real por sí mismo.

Las cosas se complican cuando la aplicación comienza a añadir sus propios niveles personalizados encima de todo eso.

En lugar de usar el framework directamente, un desarrollador recurre a un envoltorio interno alrededor de él.

Ese envoltorio, a su vez, depende de otro envoltorio más.

En poco tiempo, el modelo de programación diaria del equipo apenas se parece ya a la plataforma subyacente.

Este patrón aparece especialmente con frecuencia en organizaciones más grandes.

Un equipo podría terminar estructurando algo como esto:

<AppPage>
  <DataBoundary>
    <PermissionGate>
      <FormContainer>
        <EntityEditor />
      </FormContainer>
    </PermissionGate>
  </DataBoundary>
</AppPage>

Cada componente tiene un propósito definido.

Cada capa cuenta con una razón documentada para existir.

Pero en cuanto algo falla, un desarrollador debe reconstruir mentalmente toda la estructura antes incluso de llegar al código real de la función.

Esa reconstrucción no es gratuita, ni en términos cognitivos ni en tiempo invertido.

Los componentes genéricos suelen ser los peores culpables

Una de las formas más simples de generar complejidad en el frontend es diseñar un componente pensado para anticipar todas las necesidades futuras.

Comienza de forma inocente:

<Button />

Luego crece un poco más:

<Button variant="primary" />

Y sigue expandiéndose:

<Button
  variant="primary"
  size="large"
  loading
  icon={...}
  permission="admin"
  analyticsEvent="save"
  confirm
/>

Con el tiempo, lo que queda técnicamente ya no es un botón.

Se trata de un marco en miniatura para renderizar acciones arbitrarias.

El equipo se siente más productivo, ya que ahora se pueden crear nuevos botones mediante configuración en lugar de una implementación desde cero.

Pero la configuración sigue siendo código, independientemente de cómo parezca.

En algunos aspectos es peor, porque la configuración oculta el flujo de control real.

Al leer veinte líneas de código sencillo, se puede seguir con exactitud lo que sucede.

Al leer veinte líneas de configuración, puede ser necesario examinar la implementación del componente, rastrear el analizador de configuración, determinar los valores por defecto y averiguar qué opciones interactúan entre sí de forma silenciosa.

El código explícito ha sido reemplazado, en efecto, por un vocabulario.

Ese vocabulario puede ser realmente poderoso.

También puede convertirse fácilmente en un dialecto del que nadie quiera encargarse de mantenerlo.

La trampa de la “resistencia al futuro”

La resistencia al futuro suele ser la justificación más fuerte que alguien ofrece para añadir una abstracción.

“Podríamos necesitar esto más adelante.”

“Probablemente habrá más variantes en el futuro.”

“Esto podría reutilizarse en otras partes de la aplicación.”

“Construyámoslo de forma genérica desde el principio.”

Ocasionalmente ese instinto es correcto. Con mucha más frecuencia, simplemente no se cuenta con suficiente información como para saberlo.

El problema es que cualquier abstracción implica suposiciones sobre el problema. Abstraer demasiado pronto significa fijar elecciones arquitectónicas antes de comprender realmente lo que se está construyendo. Y una vez que otras partes del código comienzan a depender de esa abstracción, cambiar de rumbo se vuelve muy costoso rápidamente.

Este es precisamente el motivo por el cual la abstracción temprana es más arriesgada de lo que parece. El código duplicado suele ser fácil de refactorizar cuando uno está listo. Por otro lado, una abstracción defectuosa tiende a propagarse hacia afuera.

Imagínese tres componentes que se parecen. Podría dejar la duplicación por ahora sin tocarla. Si con el tiempo surge un patrón compartido real, entonces lo extrae. Pero si se pasa directamente a una abstracción generalizada, cada caso de uso futuro tendrá que adaptarse a las suposiciones hechas al principio.

En ese punto, la abstracción deja de ser una conveniencia y pasa a ser una restricción. Lo que se pretendía para eliminar la repetición acaba haciendo que los cambios sean más difíciles de lo que jamás habría sido la duplicación.

Las buenas abstracciones suelen surgir del dolor

Las abstracciones más sólidas tienden a no planificarse de antemano. Se descubren.

Un equipo implementa el mismo comportamiento más de una vez. Con el tiempo se dan cuenta de qué partes son realmente idénticas y cuáles solo parecen similares en la superficie. Entienden qué es lo que realmente difiere. Solo entonces extraen el núcleo estable y compartido.

Eso genera una base mucho más sólida. Se podría describir la secuencia de esta manera:

La duplicación conduce a la repetición, la repetición conduce al entendimiento, y el entendimiento conduce a la abstracción.

No obstante, los equipos de frontend suelen seguir un camino diferente:

La posibilidad conduce directamente a la abstracción, luego a la configuración y finalmente a la confusión.

El primer enfoque requiere más tiempo al inicio. Pero a lo largo de la vida del proyecto suele ser más rápido en general, ya que la abstracción resultante refleja el conocimiento que el equipo realmente adquirió a través de la experiencia.

No toda duplicación debe eliminarse

Esta es una verdad incómoda para los desarrolladores, ya que por defecto la duplicación suele percibirse como un error. Pero a veces la duplicación es la decisión correcta.

Imaginemos que dos componentes comparten una lógica de validación casi idéntica. Si esa lógica consta solo de cinco líneas y cada componente se rige por reglas comerciales diferentes, dejar el código duplicado puede ser la opción más inteligente.

¿Por qué? Porque mantenerlos separados preserva la comprensión local. Quien trabaje más tarde en un componente puede ajustar su comportamiento sin preocuparse por dañar accidentalmente al otro.

El código duplicado indica implícitamente: estas dos cosas se parecen en este momento por casualidad.

Una abstracción dice algo mucho más fuerte: estas dos cosas están destinadas a ser idénticas y deben cambiar juntas en el futuro.

Esa es una afirmación más importante. Solo deberías recurrir a una abstracción cuando creas genuinamente que esa afirmación es cierta.

Las abstracciones deben tener una API pequeña

Una prueba práctica útil es esta: ¿cuánto necesitas realmente aprender antes de poder usar correctamente esta abstracción?

Si esa lista sigue creciendo, probablemente estés viendo cómo una abstracción se convierte en un framework.

Una buena abstracción oculta la complejidad. Una mala una oculta las decisiones. Suenan similares, pero no son lo mismo en absoluto.

Una API bien diseñada puede parecer tan simple como esta:

const user = useUser(id);

Una menos sólida comienza a acumular banderas y opciones, terminando como esta:

const user = useUser(id, {
  cache: true,
  normalize: true,
  permissions: true,
  optimistic: false,
  retry: 3,
  suspense: false,
  transform: customTransform,
  mode: "editor",
});

En algún momento la abstracción deja de simplificar cualquier cosa. Simplemente reubica el problema original en un objeto de configuración. Ese cambio merece considerarse una señal de alerta.

Los equipos front-end necesitan un presupuesto para abstracciones

Los equipos ya hablan habitualmente sobre presupuestos de rendimiento, presupuestos de tamaño de paquetes, presupuestos de errores y presupuestos de infraestructura. Vale la pena añadir un presupuesto para abstracciones a esa lista.

El objetivo no es necesariamente tener menos abstracciones en términos absolutos. Se trata de mantener el número de abstracciones dentro de lo que el equipo puede manejar cómodamente en su mente.

Antes de agregar uno nuevo, es útil hacerse algunas preguntas.

¿Cuántas veces ya ha aparecido este patrón exacto? Si la respuesta es una vez, no lo abstraiga aún. Si son dos veces, considérelo como motivo de sospecha y no como acción inmediata. Cinco apariciones comienzan a parecer evidencia real.

A continuación: ¿cambian realmente estas cosas por las mismas razones? Que se parezcan no es una justificación suficiente. Dos componentes pueden parecer casi idénticos mientras evolucionan por caminos completamente diferentes por razones empresariales distintas; en ese caso, agruparlos en una sola abstracción podría ser un error.

Finalmente: ¿esta abstracción realmente simplifica los casos cotidianos? No se refiere a algún caso hipotético futuro, sino a aquellos con los que las personas se encontrarán constantemente. Si usar la abstracción implica leer su documentación cada vez, es probable que haya cambiado un problema por uno peor.

El mejor código frontend suele ser aburrido

El desarrollo de software tiene una curiosa jerarquía de estatus incorporada. Una abstracción ingeniosa parece más impresionante que tres componentes sencillos. Un sistema genérico se percibe como más serio desde el punto de vista arquitectónico que una función simple. Un framework reutilizable parece más profesional que un poco de código duplicado.

Pero los sistemas en producción no recompensan al código por lucir sofisticado. Recompensan al código que los desarrolladores pueden modificar con seguridad.

El código frontend más valioso suele ser de ese tipo aburrido. Al abrir un componente, puedes ver inmediatamente de dónde provienen sus datos. Puedes saber exactamente qué se activa cuando un usuario hace clic en algo. Puedes editar el marcado directamente. Puedes rastrear cómo fluye el estado. Puedes localizar la llamada a la API sin dificultad.

No deberías tener que absorber la filosofía de diseño interna de un equipo solo para corregir un error. Eso no es señal de ingeniería poco sofisticada, sino de una buena ingeniería.

La abstracción debe reducir el pensamiento, no aumentarlo

La abstracción no existe para hacer que el código parezca reutilizable. Su propósito es facilitar la comprensión de un sistema. Ese debe ser el estándar que los equipos frontend deben aplicar a cada abstracción.

Si una abstracción permite que diez componentes compartan un comportamiento complejo sin obligar a cada desarrollador a entender cómo se implementa ese comportamiento, entonces cumple su función. Si, por el contrario, cada desarrollador tiene que aprender una API complicada solo para modificar un componente simple, es probable que la abstracción esté trabajando en contra de ustedes.

Esta distinción es importante porque el trabajo en frontend ya es inherentemente difícil. Los navegadores son complicados. Las interfaces de usuario son complicadas. Mantener el estado sincronizado es complicado. La accesibilidad es complicada. El rendimiento es complicado. No hay necesidad de añadir más complejidad encima de todo eso solo para que la arquitectura parezca más sofisticada en teoría.

La próxima etapa del desarrollo frontend probablemente no surgirá de descubrir otra capa de abstracción. Surgirá de mejorar la capacidad para reconocer cuándo no es necesario crear una.

Los ingenieros que se destacarán no serán necesariamente aquellos que puedan diseñar el sistema de componentes reutilizables más elaborado. Serán aquellos que puedan analizar un problema y juzgar correctamente si realmente se necesita un sistema para resolverlo.

A veces, la abstracción adecuada es una sola función. Otras veces, es un componente. En ocasiones, se trata simplemente de un módulo bien nombrado. Y a veces, son cinco líneas de código duplicado que cualquier miembro del equipo puede leer y comprender de inmediato.

Diferenciar esas situaciones es la verdadera habilidad que vale la pena desarrollar.

Lecturas relacionadas

  • Patrones de Diseño React: De OOP Clásico a Gancho Modernos — Explica cómo los patrones de software clásicos como Singleton, Factory y Observer se aplican en React, junto con patrones específicos de React como HOCs, Hooks y Componentes Compuestos.
  • Comprendiendo los Principios SOLID a Través de Ejemplos Prácticos de Código — Esta guía detalla los cinco principios SOLID con ejemplos de código concretos, mostrando cómo se aplican en proyectos reales y aplicaciones React.
  • Seis técnicas de TypeScript que convierten los tipos en una verdadera prevención de errores — Aprenda cómo las uniones con etiquetas, los tipos derivados, “never checks”, “unknown” e IDs marcados permiten a TypeScript detectar errores reales en tiempo de compilación en lugar de en producción.