Duplicación versus acoplamiento: cómo decidir cuándo debe existir código compartido
Aprenda por qué combinar componentes React o servicios NestJS similares puede resultar más costoso que duplicarlos, y utilice tres preguntas para decidir cuándo una abstracción merece su lugar.
La mayoría de los desarrolladores están entrenados para considerar el código repetido como un defecto: identifican dos componentes similares, extraen uno común y siguen adelante. Ese reflejo suele ser correcto, pero oculta un costo que solo se manifiesta meses después, cuando esa parte compartida debe servir a usuarios cuyos requisitos han cambiado. Al enmarcar la decisión como un equilibrio entre dos costos diferentes, la duplicación y el acoplamiento, se obtiene un pequeño conjunto de preguntas para decidir cuál pagar en código React, React Native y backend.
La idea central es simple pero fácil de malinterpretar: la repetición no es una virtud, pero en algunas situaciones la duplicación es más económica que el acoplamiento. A continuación se presenta una guía para reconocer esas situaciones.
Cómo un componente compartido bien estructurado se convierte en un lenguaje de configuración
Comience con dos componentes que muestran la imagen de una persona. Uno es para usuarios regulares:
<UserAvatar user={user} />
El otro es para los instructores:
<TrainerAvatar trainer={trainer} />
El primer día son casi indistinguibles. Cada uno muestra:
- una imagen de perfil
- un contenido alternativo cuando falta la imagen
- dimensiones idénticas
- un estado de carga
La conclusión obvia es que un único componente debería hacer ambas funciones, por lo que aparece un avatar genérico:
<Avatar
imageUrl={...}
fallback={...}
size="medium"
/>
Parecía una solución clara: menos código y un único lugar para corregir errores. Pero luego el producto evoluciona. Los usuarios necesitan un marcador temporal que respete la configuración de privacidad. Los instructores necesitan una insignia de verificación. Los usuarios pueden recurrir a sus iniciales. Los instructores deben indicar si están disponibles en ese momento. Cada solicitud llega al componente compartido como otro parámetro:
<Avatar
imageUrl={...}
fallback={...}
size="medium"
showInitials={...}
showVerification={...}
showAvailability={...}
privacyMode={...}
/>
Siguen llegando más requisitos, y cada uno se convierte en un problema adicional. Con el tiempo, el avatar “genérico” termina teniendo que manejar información sobre usuarios, entrenadores, reglas de privacidad, verificaciones, disponibilidad y toda clase de lógica empresarial que no tiene nada que ver con dibujar un círculo con una imagen dentro. Aún es técnicamente reutilizable, pero ya no es sencillo. La complejidad nunca se eliminó; simplemente se acumuló en un único archivo, del cual ahora depende todo el sistema. Si este patrón de explosión de propiedades le resulta familiar, el artículo sobre cuándo los componentes React reutilizables causan problemas analiza con más profundidad cómo refactorizar dichos componentes.
Compartir código significa compartir el futuro
El punto que rara vez se discute es que extraer código compartido no es solo una decisión de implementación. Vincula los futuros de los consumidores entre sí. Cuando el componente A y el componente B dependen ambos de una misma abstracción, cualquier cambio realizado en A ahora puede dañar o modificar a B. La reutilización crea una relación, y las relaciones representan acoplamiento.
Eso cambia la pregunta que merece hacerse. “¿Podrían estas dos cosas compartir código?”, casi siempre se puede responder con sí. La pregunta más adecuada es: ¿deberían estas dos cosas compartir un futuro?
Dos bloques de código pueden ser textualmente idénticos hoy en día y aún así pertenecer a partes no relacionadas del dominio. Su implementación coincide; sus razones para cambiar pueden no hacerlo. Esa diferencia predice los costos de mantenimiento mucho mejor que un simple conteo de líneas duplicadas. Está estrechamente relacionada con el principio de responsabilidad única, que suele expresarse como “un módulo debe tener una sola razón para cambiar”, donde una razón para cambiar se refiere a un grupo de personas cuyas solicitudes lo impulsan.
Cuando el marcado duplicado garantiza independencia
Considere una tarjeta de usuario:
<UserCard />
y una tarjeta de pago:
<PaymentCard />
Por ahora, ambas muestran la misma estructura:
<div className="card">
<h3>{title}</h3>
<p>{description}</p>
</div>
Una tarjeta compartida es el siguiente paso lógico:
<Card />
Supongamos ahora que la tarjeta dirigida al usuario se rediseña con frecuencia, mientras que la tarjeta de pago está sujeta a requisitos de producto y cumplimiento completamente distintos. La marcación correspondiente es una coincidencia; el significado empresarial no se comparte. Mantenerlas separadas implica unos pocos renglones duplicados de JSX. A cambio, cada concepto tiene un espacio donde puede evolucionar sin necesidad de negociación. Esa duplicación aporta algo concreto: la posibilidad de modificar uno sin tener que coordinarse con los responsables del otro. Visto de esta manera, dos componentes separados no constituyen una arquitectura descuidada. Pueden ser un límite deliberado.
Existe una nuanza importante específicamente para React. Una capa puramente visual, un contenedor con estilo que no tiene significado en el contexto del dominio, a menudo puede compartirse de forma segura, ya que su motivo para cambiar es el sistema de diseño y no una característica del producto. La trampa ocurre cuando la parte compartida comienza a absorber decisiones relacionadas con el dominio que pertenecen a un único usuario.
Pregúntate por qué dos cosas son iguales, no cómo fusionarlas
La experiencia suele hacer que cambies la primera pregunta que haces al ver repetición. Al principio, el instinto es “¿cómo puedo abstraer esto?”. Una secuencia más útil es:
- ¿Por qué estas dos cosas son iguales en este momento?
- ¿Seguirán siendo iguales, y por la misma razón?
La segunda pregunta es más difícil, porque te obliga a dejar de comparar la sintaxis y empezar a comparar la intención.
Implementaciones idénticas pueden representar conceptos diferentes
Tome dos ayudantes de formato, uno para usuarios:
formatUserName(user)
y otro para formadores:
formatTrainerName(trainer)
Supongamos que ambos contienen actualmente el mismo contenido:
return `${firstName} ${lastName}`;
Por lo que se fusionan en un único ayudante:
formatFullName(person)
Eso parece inofensivo hasta que las reglas difieren. Los nombres de usuario pueden requerir tener en cuenta:
- nombres preferidos
- configuraciones de privacidad
- reglas de localización
Los nombres de formadores pueden necesitar:
- títulos profesionales
- certificaciones
- políticas específicas de nombre a mostrar
Las dos funciones originales podrían haber evolucionado por caminos separados sin problemas. En cambio, el ayudante genérico afirma que los usuarios y formadores son del mismo tipo de “persona” a efectos de nombramiento, lo cual ya no es cierto. Este es el riesgo menos evidente de la abstracción:
Una abstracción no solo comparte código; también codifica una afirmación sobre cómo está estructurado el dominio.
Cuando esa afirmación es incorrecta, la abstracción engaña activamente al siguiente desarrollador, quien asume razonablemente que cualquier cosa llamada formatFullName se aplica a todas las personas del sistema.
La consecuencia de una abstracción prematura llega más tarde
La abstracción prematura resulta atractiva porque todas las métricas visibles mejoran el día en que se introduce. El archivo pasa de algo como:
100 lines
a:
60 lines
La duplicación desaparece, la solicitud de integración se ve más limpia y la abstracción parece elegante. El costo se pospone. Un año después, alguien necesita cambiar el comportamiento para un consumidor y descubre que ese componente compartido se utiliza también en siete otros casos. En lugar de arriesgarse a dañarlos, crean una rama separada:
if (variant === "A") ...
else if (variant === "B") ...
else if (variant === "C") ...
Luego aparece otra propiedad, otra bandera, un camino de compatibilidad para pantallas antiguas. La abstracción sobrevive, pero su simplicidad conceptual no. Así es como los conjuntos de código terminan con componentes cuya interfaz se ve así:
<UniversalThing
mode="..."
variant="..."
type="..."
compact
showHeader
showFooter
enableSomething
disableSomethingElse
/>
En ese punto, el componente deja de ser una abstracción y se convierte en un pequeño lenguaje de configuración para varios casos de uso no relacionados. Cada combinación de esas propiedades representa un estado sobre el cual alguien debe razonar, y la mayoría de las combinaciones nunca fueron probadas.
La reutilización puede dificultar los cambios, no facilitarlos
La ironía es que el código compartido se crea para hacer que los cambios sean más económicos, pero un uso excesivo del mismo a menudo lo hace más costoso. La razón es el radio de impacto: el conjunto de elementos que puede verse afectado por un único cambio.
Antes de la abstracción, cada funcionalidad tenía su propio componente:
Feature A → Component A
Feature B → Component B
Después de eso, cada funcionalidad pasa por una parte compartida:
Feature A ─┐
Feature B ─┼→ Shared Component
Feature C ─┘
El segundo diagrama tiene menos código duplicado, pero una red de dependencias más compleja. Por lo tanto, la comparación útil no es “¿qué versión tiene menos duplicaciones?”, sino “¿cuál versión hace que el costo de los cambios futuros sea más predecible?” Cuando un cambio en la funcionalidad B debe probarse mediante pruebas de regresión con las funcionalidades A y C, el componente compartido hace que los cambios sean menos predecibles, aunque haya acortado el código.
Por qué React fomenta el uso prematuro de componentes compartidos
React hace que la extracción de código sea casi sin fricción. Aparece un botón:
<Button />
y se vuelve reutilizable. Luego, una tarjeta:
<Card />
Luego, un modal:
Modal />
Luego, un campo de formulario:
FormField />
Luego, un hook personalizado:
useSomething()
Pronto surge una biblioteca de componentes interna que se utiliza en toda la aplicación. Gran parte de ella es realmente valiosa. Hay elementos que claramente deben compartirse:
- un botón que refleje de manera consistente el sistema de diseño de la aplicación
- primitivas de accesibilidad de bajo nivel, como la gestión del foco o las etiquetas accesibles
- conceptos del dominio que son verdaderamente estables
El reutilizar elementos en sí no es el problema. El problema está en reutilizarlos solo porque se parecen. Las primitivas de interfaz compartidas funcionan bien cuando están libres de reglas empresariales, un punto también abordado en diseñar componentes basados en responsabilidades y no en reutilización.
React Native multiplica los estados
Las aplicaciones móviles añaden otra dimensión. Dos pantallas pueden parecerse mientras tienen necesidades muy diferentes en su ciclo de vida. Un componente que funciona bien en una pantalla podría tener que lidiar posteriormente con:
- el hecho de que la aplicación pase al segundo plano
- el comportamiento del teclado
- la interrupción de las conexiones de red
- las diferencias entre iOS y Android
- los permisos
- los enlaces profundos
- las distintas dimensiones de los dispositivos
- el estado de navegación
- el modo sin conexión
Si todo se diseña de forma genérica desde el principio, el componente compartido acaba sabiendo sobre cada entorno en el que podría ejecutarse. Se vuelve “flexible”, y la flexibilidad tiene un precio: cada nueva opción multiplica el número de estados posibles. Cada uno de esos estados es algo que alguien eventualmente tiene que entender, probar y mantener. Dos opciones crean cuatro combinaciones; cinco crean treinta y dos.
Los servicios de backend caen en la misma trampa
Esto no es un problema exclusivo del frontend. Imagine dos servicios de NestJS:
UserService
TrainerService
Inicialmente pueden exponer las mismas operaciones:
create()
findById()
update()
delete()
Una clase base genérica parece atractiva:
BaseService<T>
A veces eso funciona y permite ahorrar código real. Pero una vez que las reglas de negocio para usuarios y formadores difieren, la clase base comienza a acumular excepciones. Primero una verificación de tipo:
if (entityType === "user") ...
luego un gancho sobrescribible antes de las actualizaciones:
protected beforeUpdate(...)
y otro después de la creación:
protected afterCreate(...)
Y gradualmente se crea un conjunto completo de puntos de extensión cuyo único propósito es hacer que un servicio genérico se comporte de manera diferente según el dominio. Esto sustituye el código duplicado por una complejidad condicional, que suele ser mucho más difícil de comprender, ya que entender el comportamiento de una entidad ahora implica leer la clase base, sus ganchos y todas las sobrescripciones juntas. Si esto le resulta familiar, la discusión sobre estructurar dominios en NestJS con reglas de DDD ofrece una perspectiva complementaria sobre dónde deben situarse los límites.
Esto no es un argumento a favor del copiar y pegar
Concluir que la duplicación es algo bueno sería igualmente simplista. La duplicación conlleva costos reales:
- Si diez lugares implementan la misma regla de negocio de forma independiente, una corrección de error puede requerir diez modificaciones, y omitir alguna genera un comportamiento inconsistente.
- Si veinte componentes implementan cada uno el mismo comportamiento de accesibilidad, mantener su consistencia se vuelve muy difícil.
- Si varias aplicaciones dependen del mismo contrato estable, compartir ese contrato es extremadamente valioso.
El punto real es más específico: la duplicación y el acoplamiento son dos tipos diferentes de costos. Una buena ingeniería implica elegir el costo que se adapte al problema, en lugar de minimizar siempre uno de ellos.
Tres preguntas antes de extraer una abstracción
Cuando dos fragmentos de código se parecen, deténgase y analícelas antes de fusionarlos.
¿Cambian por la misma razón?
Este es el más importante. Si los requisitos del producto tienden a cambiar A y B juntos, compartirlos suele ser sensato. Si A cambia debido a un factor empresarial y B por otro, la implementación idéntica de hoy no es una prueba sólida de que pertenezcan al mismo conjunto.
¿Significan lo mismo?
El código puede ser idéntico mientras que sus semánticas difieren, y son las semánticas las que evolucionan. Un precio y un saldo de cuenta pueden ser ambos números simples, pero eso no justifica combinarlos en un único alias en todas partes:
type Amount = number;
La representación es la misma; el significado, no. Un precio puede incluir reglas de moneda e impuestos, mientras que un saldo puede tener límites de sobregiro. Mantenerlos como conceptos distintos, incluso si hoy son números, permite que esa divergencia tenga un costo bajo.
¿Esto elimina la duplicación o solo elimina líneas de código?
Estos son resultados diferentes. Una buena abstracción elimina los conceptos duplicados. Una mala abstracción simplemente acorta los archivos. Extraer 30 líneas de dos componentes y colocarlas en una función auxiliar de 40 líneas con ocho propiedades no necesariamente mejora nada; la complejidad simplemente se ha trasladado y ahora hay una interfaz que mantener.
La duplicación intencionada es una opción legítima
Es razonable dejar dos fragmentos de código separados cuando:
- son pequeños
- sencillos
- tienen probabilidades de evolucionar de forma independiente
- no protegen una invariante crítica
- no forman parte de un concepto de dominio compartido
La razón para dejarlos en paz no es una falta de habilidad con las abstracciones, sino una comprensión del costo que implicaría dicha abstracción. Ser capaz de observar una repetición y decidir “todavía no” es señal de madurez, no de pereza. No toda repetición constituye deuda técnica; a veces es simplemente repetición.
Cuando la duplicación se convierte en una señal de alerta
El otro aspecto es igualmente importante. Algunas duplicaciones son una señal clara de que es necesario consolidar:
- la misma regla de negocio compleja copiada en varios lugares
- tres aplicaciones que implementan cada una el mismo flujo de autenticación
- múltiples equipos que deben cumplir con un mismo contrato de API
- un cambio en una regla que requiere recordar diez ubicaciones distintas
En esos casos, la pregunta correcta es: ¿qué conocimiento se está duplicando? Eso es mucho más importante que cuántas líneas se repiten, porque lo que hay que evitar duplicar es el conocimiento, no el texto.
DRY siempre se refirió al conocimiento
"No te repitas a ti mismo" se interpreta comúnmente como ">nunca escribas el mismo código dos veces". La formulación original en The Pragmatic Programmer se refiere al conocimiento: cada fragmento de conocimiento debe tener una única representación autorizada en un sistema. Esas son reglas diferentes.
Dos componentes pueden contener JSX similar sin duplicar ningún conocimiento empresarial. Al mismo tiempo, dos funciones que no se parecen en nada pueden codificar cada una la misma regla de negocio, por ejemplo un umbral de descuento codificado directamente tanto en un componente de pago como en un validador backend. El segundo caso es el peligroso, porque causará desviaciones silenciosas. Por lo tanto, en lugar de preguntarse si algo puede ser reutilizable, hay que preguntarse donde debería residir ese conocimiento.
Deje que la abstracción se imponga por sí misma
No es necesario diseñar una abstracción en el momento en que aparece la duplicación. Permitir que la repetición exista durante un tiempo y observar cómo evolucionan las copias es una estrategia válida. Si los casos de uso segundo y tercero siguen avanzando en la misma dirección, la forma adecuada se vuelve evidente. El comportamiento compartido se descubre en lugar de inventarse.
Esa diferencia es significativa. Una abstracción extraída de tres casos de uso verdaderamente similares suele ser mucho más sólida que una diseñada a partir de un único caso de uso para adaptarse a dos hipotéticos casos futuros. Este es el razonamiento detrás de la conocida heurística de la “regla de los tres”. Dicho de otra manera:
Abstraiga basándose en lo que ahora entiende que es el código, no en lo que imagina que podría llegar a ser.
El objetivo es un diseño adaptable, no código reutilizable
La reutilización por sí sola no es una medida útil de la calidad arquitectónica. Indicadores mejores son:
- qué tan fácil es comprender el código
- qué tan aislada está un cambio típico
- qué tan predecible es el alcance de un cambio
- si los conceptos empresariales permanecen claramente separados
- si los límites se encuentran en lugares razonables
A veces esos criterios conducen a una abstracción compartida elegante. Otras veces generan dos componentes casi idénticos uno al lado del otro, y eso puede ser un diseño mejor, porque los dos nunca fueron lo mismo. Simplemente se parecen *hoy*.
Puntos clave
- Compartir código crea una dependencia entre los consumidores; considere cada extracción como una decisión de que sus futuros están vinculados.
- Juzgue las opciones para la abstracción por su motivo de cambio y su significado, no por la similitud textual.
- Las banderas de propuesta, las ramas variantes y los ganchos sobrescriptibles son señales de que una pieza compartida sirve a dominios no relacionados.
- Duplicar código pequeño, simple y que evoluciona de forma independiente sin problemas; consolide el conocimiento duplicado como reglas de negocio, contratos y flujos de seguridad.
- Preferir las abstracciones obtenidas a partir de varios casos de uso reales en lugar de aquellas diseñadas para situaciones imaginarias.
- Antes de combinar dos elementos similares, pregúntese si está creando reutilización o estableciendo una relación, y si esa relación debería sobrevivir más allá de los límites del código que está guardando.
Lecturas relacionadas
- Cuando la IA escribe su app React pero ignora los principios de código limpio — Aprenda siete hábitos de código limpio—DRY, responsabilidad única, cláusulas de protección y más—que el código React generado por IA suele violar y cómo corregirlos.
- Cuando los componentes React reutilizables fallan: explosión de propiedades y la solución — Descubra cómo el reuso prematuro convierte a un componente React simple en una carga excesiva de propiedades, y cómo la duplicación, los componentes compuestos y la Regla de Tres lo evitan.