Inicio / Artículos

Siete señales de advertencia en las revisiones que predicen cambios costosos en el futuro

Aprenda a identificar siete problemas comunes en el código que los revisores experimentados señalan tempranamente, desde flags de modo booleano hasta errores ignorados, y cómo determinar cuándo cada uno representa un problema real.

Editor: REACTAPP.TOP13 min de lectura3031 palabras

Fuentes: 7 Code Smells Senior Engineers Catch Immediately

El código más problemático no parece estar roto. Se compila, pasa sus pruebas y funciona bien en una demostración, pero los revisores experimentados suelen poder determinar en cuestión de minutos que el próximo cambio, el siguiente incidente o la próxima transferencia de responsabilidades causará problemas. Esta guía explica siete de las señales a las que reaccionan, detalla qué predice cada una y muestra cómo distinguir un problema real de un patrón que solo parece sospechoso, para que pueda aplicar el mismo criterio en sus propias revisiones.

Qué realmente le indica un “olor a código”

Un revisor experimentado rara vez necesita leer todo un módulo para detectar problemas. Un argumento booleano cuyo propósito es invisible donde se llama a la función, una utilidad que ha adquirido una docena de comportamientos opcionales, un catch que convierte cada fallo en el mismo valor de apariencia inocente: nada de esto rompe nada hoy en día. El problema está en lo que exigirán mañana.

Ese instinto proviene de la experiencia y no de una guía de estilo más estricta. Las personas que han mantenido sistemas durante años han visto cómo una regla copiada se transforma en tres versiones contradictorias, cómo una abstracción útil se convierte en el lugar donde terminan todos los casos especiales, y cómo una solución por defecto oculta una interrupción en producción durante semanas. Lo que han aprendido a detectar es dónde un fragmento de código acumula incertidumbre.

Es útil tratar un olor a como una indicación, no como un veredicto. No demuestra que la implementación esté equivocada; indica que el código merece un examen más detallado:

  • ¿Qué persona o módulo es responsable de esta decisión?
  • ¿Cómo se mantendrá esto cuando cambien los requisitos?
  • ¿Seguirá siendo visible un fallo cuando ocurra?
  • ¿Podrá el próximo desarrollador entender por qué el código está estructurado de esta manera sin tener que averiguar quién lo escribió?

Las siete señales que se mencionan a continuación son las que con mayor frecuencia se convierten en costos si nadie plantea esas preguntas desde el principio.

1. Una función que realiza varias tareas no relacionadas

Las funciones largas generan críticas, pero su extensión rara vez es lo que preocupa a un revisor experimentado. Una función puede llegar a tener muchas líneas porque realiza una operación clara en una secuencia de pasos fáciles de entender. Otra puede caber en veinte líneas y seguir siendo difícil de trabajar, ya que maneja al mismo tiempo la validación de entradas, las verificaciones de permisos, el almacenamiento en la base de datos, la formateación de la respuesta, la activación de notificaciones y la traducción de errores.

El verdadero problema es que las responsabilidades mixtas hacen que una función tenga muchas razones independientes para cambiar. Una nueva regla de acceso la afecta, al igual que un cambio en el formato de la respuesta o una migración de esquema. Incluso un nuevo requisito de notificación la afecta. Las preocupaciones que deberían evolucionar por separado terminan concentrándose en un mismo lugar, y hasta una edición menor obliga a comprender todo el flujo.

Un indicio útil es un nombre que no puede describir honestamente la función. Algo llamado processOrder podría verificar al cliente, calcular precios, gestionar existencias, crear una intención de pago, guardar el pedido y enviar un correo de confirmación. El nombre se mantiene vago porque uno preciso requeriría un párrafo entero para describirlo.

Por qué la extracción mecánica no es la solución

Dividir la función en partes más pequeñas no ayuda automáticamente. Extraer fragmentos según el conteo de líneas puede dispersar la misma confusión en más archivos, mientras que todas las dependencias ocultas siguen existiendo. Un enfoque mejor es buscar límites que tengan sentido:

  • ¿Dónde se toma una decisión empresarial?
  • ¿Qué parte representa un efecto secundario en un sistema externo?
  • ¿Qué parte corresponde a una transformación pura de datos?
  • ¿Qué parte gestiona la transacción?

Una vez que esas uniones se vuelven visibles, el código puede organizarse de acuerdo con la forma en que realmente funciona la operación. El problema subyacente nunca fue la cantidad de trabajo que realiza la función, sino el hecho de que nadie podía cambiar una sola decisión sin tener en cuenta varias decisiones no relacionadas al mismo tiempo.

2. Parámetros booleanos que activan modos ocultos

Un argumento booleano parece inofensivo porque contiene un solo bit. En la práctica, con frecuencia oculta todo un flujo de trabajo alternativo bajo el nombre de una función existente. Considere una llamada como la siguiente, que casi no informa al lector sobre lo que ocurrirá:

await updateAccount(accountId, true, false);

Para entenderlo, hay que abrir la implementación y averiguar si el primer true fuerza la actualización, dispara una notificación, omite la validación, conserva el historial o activa la cuenta. Quien escribió la función puede recordarlo, pero todos los demás deben reconstruir su significado a partir de las posiciones de los argumentos, y un par de valores literales intercambiados pasará por todas las comprobaciones de tipo.

Combinaciones que nadie diseñó

El problema se agrava a medida que se acumulan las banderas. Tres valores booleanos generan ocho combinaciones posibles, varias de las cuales nadie jamás consideró. Si una función acepta forceUpdate, skipValidation y sendNotification, ¿qué debería suceder cuando una actualización forzada omite la validación pero aún así notifica a alguien? El sistema de tipos permite felizmente esa llamada incluso cuando el dominio empresarial no debería permitirlo.

Los revisores marcan esto porque un conjunto de indicadores suele significar que una función reemplaza a varias operaciones. Pueden compartir componentes internos, pero no comparten la misma finalidad. Editar detalles de contacto, verificar una cuenta y aplicar una anulación administrativa pueden escribir en la misma fila, pero ningún usuario debería necesitar recordar qué patrón de indicadores selecciona qué flujo de trabajo.

Elegir la refactorización adecuada

En algunos casos, basta con pasar a un objeto de argumento único cuyas propiedades tengan nombres, ya que updateAccount(id, { notify: true }) al menos se explica por sí mismo en el lugar donde se llama. En otros casos, funciones separadas como verifyAccount y overrideAccount hacen que la intención del negocio sea explícita y eliminan por completo las combinaciones imposibles. La opción adecuada depende de si los comportamientos son variaciones reales de una misma operación o operaciones distintas que casualmente comparten código.

Los booleanos no son inherentemente malos. Una opción con un nombre claro como includeArchived es perfectamente razonable. La señal de advertencia es una bandera que cambia el significado de la función en lugar de modificar un detalle ya bien comprendido.

3. La misma regla de negocio expresada en diferentes formas

El texto copiado y pegado es fácil de detectar. El conocimiento duplicado es más peligroso, porque cada copia puede parecer completamente diferente.

Tomemos una regla de permisos. Puede aparecer como un botón oculto en la interfaz de usuario, una verificación de rol en un controlador, una condición de estado en un servicio y una cláusula WHERE en una consulta. Cada capa parece proteger el sistema, pero cada una puede contener una idea ligeramente distinta sobre quién tiene permiso para actuar.

Todo funciona bien hasta que cambia la política. Un punto de extremo adopta la nueva regla, otro mantiene la lógica antigua, y un trabajo programado elude ambos al dirigirse directamente a un servicio interno. La interfaz ahora ofrece una acción que el servidor rechaza. Una parte de la aplicación trata a un usuario como autorizado debido a su rol, mientras que otra insiste en que debe ser el propietario del registro.

Los revisores experimentados detectan combinaciones recurrentes de estados, roles, indicadores y fechas, ya que tales condiciones suelen representar el mismo significado empresarial escrito más de una vez. Preguntan dónde se encuentra la decisión autoritativa y si las demás capas la aplican o la redefinen silenciosamente.

Está bien que las capas tengan propósitos diferentes

Esto no significa que una regla solo pueda aparecer en una línea de código en todo el sistema. La interfaz de usuario puede reflejar la autorización para mejorar la usabilidad, mientras que el backend debe aplicarla por motivos de seguridad. Una restricción de base de datos puede proteger la misma invariante a nivel de almacenamiento. Esas capas tienen funciones distintas. Aparece el problema cuando cada una responde a la pregunta empresarial según sus propios criterios.

Un diseño saludable asigna a la regla un único responsable designado y permite que las demás partes consuman, apliquen o muestren el resultado. Cuando la responsabilidad no está clara, cada cambio futuro en las políticas se convierte en una tarea de búsqueda: hay que localizar todos los lugares donde la regla está codificada de forma discreta y confiar en haber encontradolas todas. Quienes han experimentado la evolución gradual de las políticas saben que adquirir conocimiento repetidamente suele costar más que modificar el código una y otra vez.

4. Un ayudante compartido con un objeto de opciones en constante crecimiento

Muchas abstracciones problemáticas comienzan como ayudantes razonables. Cuando dos lugares utilizan lógica similar, se extrae la parte común, lo que da como resultado algo pequeño, enfocado y fácil de seguir.

Luego, los dos solicitantes comienzan a querer cosas diferentes. Uno necesita intentos repetidos; el otro nunca debe volver a enviar su solicitud. Uno se beneficia del almacenamiento en caché; el otro nunca debe ver valores obsoletos. Uno devuelve una carga útil completa; el otro la transmite de forma por lotes. En lugar de dividir esos flujos de trabajo, la herramienta genera una nueva opción para cada diferencia.

En poco tiempo, el objeto de opciones abarca la traducción de errores, la remodelación de resultados, los ajustes del caché, el conteo de intentos, el manejo de autenticación, las funciones de enlace y un puñado de interruptores booleanos. La mayoría de los solicitantes utilizan dos o tres de ellos, pero cualquier cambio corre el riesgo de afectar a todos.

Esto se debe a que la abstracción ha dejado de eliminar la complejidad y ha comenzado a concentrarla. Los puntos de llamada parecen sencillos, pero comprender el sistema ahora implica saber cómo cada opción interactúa con todas las demás. Un gran número de llamadas no es evidencia de que la abstracción sea sólida; a veces solo demuestra que el helper se ha vuelto inevitable.

Sintaxis similar versus comportamiento compartido

La pregunta clave es si los puntos de llamada comparten un comportamiento estable o simplemente código de apariencia similar. Dos operaciones pueden realizar solicitudes HTTP, pero tratar los fallos y las reintentos de maneras opuestas. Un par de formularios puede tener los mismos campos, pero una validación, un proceso de envío y un manejo de borradores diferentes. Fusionarlos elimina la duplicación visible, pero hace que cada diferencia real sea más difícil de expresar.

Lo que suele funcionar mejor es una abstracción más específica introducida posteriormente. En lugar de compartir todo el flujo de trabajo, comparta una sola parte estable, como una transformación, una política o una llamada de bajo nivel, y mantenga los flujos de trabajo distintos de forma explícita. La reutilización sigue siendo valiosa; lo que debería generar preocupación es un patrón en el que cada nueva exigencia añade otra opción de la cual los usuarios existentes ni la necesitan ni la entienden. Para una visión centrada en el frontend del mismo problema, consulte por qué las abstracciones del frontend se convierten silenciosamente en deuda técnica.

5. Manejo de errores que disfraza el fracaso como éxito

Un bloque catch puede hacer que una aplicación parezca sólida mientras erosiona silenciosamente la confianza en ella.

Imagínese una capa de acceso a datos que responde a un error en la base de datos devolviendo []. O un servicio cuya llamada HTTP falla y que simplemente devuelve null. El frontend mapea cada solicitud rechazada a un estado vacío. Nada se cae, pero el sistema ya no puede distinguir entre una ausencia legítima de datos y un fallo en el funcionamiento.

Los revisores detectan esto rápidamente porque saben cómo se desarrolla durante un incidente. Un panel de control indica cero registros mientras la base de datos está inactiva. Un cliente ve “No se encontraron pedidos” después de que su sesión haya expirado silenciosamente. Una tarea en segundo plano marca un elemento como completado porque una dependencia no devolvió nada utilizable. La interfaz permanece tranquila porque ha reemplazado la verdad por una respuesta creíble.

Preguntas que hacer a cada bloque de captura

Al revisar el manejo de errores, verifique tres cosas:

  • ¿Se mantiene la causa original, por ejemplo como la cause de un nuevo error, o al menos en los registros?
  • ¿Puede quien llama distinguir un resultado empresarial esperado de una falla técnica?
  • ¿La capa que captura el error es realmente la responsable de la siguiente decisión?

Cuando el código en ese nivel no tiene forma de recuperarse, intentarlo de nuevo, deshacer parte del trabajo realizado o proporcionar un sustituto razonable, capturar el error podría no servir para nada más que destruir pruebas.

Nada de esto significa que las excepciones en bruto deban llegar a los usuarios. Los mensajes mostrados a las personas deben ser seguros y comprensibles, y los errores internos suelen necesitar ser traducidos a categorías de dominio estables. El requisito es que la traducción conserve suficiente significado para que tanto la aplicación como los desarrolladores puedan responder correctamente.

Un resultado vacío debería indicar que la operación tuvo éxito y no encontró nada. Una operación fallida debe seguir siendo una falla, a menos que el sistema haya definido deliberadamente un mecanismo de respaldo seguro. Combinar ambas situaciones por conveniencia ralentiza la depuración y muestra a los usuarios información engañosa. Un buen manejo de errores incluye la detección de fallos; un mal manejo de errores los oculta.

6. Un requisito sencillo con diferencias amplias e irrelevantes

Los revisores experimentados suelen observar la estructura general de una solicitud de integración antes de leer las líneas individuales. Cuando un requisito modesto se distribuye en una larga lista de archivos no relacionados, eso suele indicar un acoplamiento oculto o una tarea que se ha desviado de su objetivo original.

Añadir un único campo podría llegar legítimamente a la persistencia, los servicios, las respuestas de API y la interfaz de usuario; se trata de un cambio coherente en todos los niveles. El problema surge cuando la misma solicitud de integración también reescribe utilidades compartidas, renombra tipos no relacionados, modifica el manejo de errores, mueve módulos de un lugar a otro y actualiza archivos de snapshot grandes.

Cada una de esas ediciones podría ser una mejora, pero juntas agrupan varias razones para el cambio en una sola unidad. Los revisores deben evaluar simultáneamente la nueva funcionalidad, la refactorización, el cambio en los contratos y las tareas de limpieza. Si se produce una regresión, resulta más difícil identificar la edición responsable. Si es necesario revertir la funcionalidad, las mejoras no relacionadas también se revierten junto con ella.

Por qué la programación asistida por IA hace que esto sea más común

Las herramientas de generación de código facilitan que se caiga en este patrón, ya que reescribir completamente cuesta casi nada. Si pides la corrección de un solo error, es posible que recibas una función reemplazante pulida con nombres nuevos, un flujo de control diferente, abstracciones actualizadas y un comportamiento ante errores modificado. Producirla costó casi nada; sin embargo, verificarla sigue requiriendo del equipo el mismo esfuerzo de revisión que siempre.

Una buena regla es mantener el tamaño de un cambio en proporción a la claridad con la que se pueda explicar. Un diagnóstico específico suele requerir una solución similarmente precisa, mientras que las tareas de limpieza más amplias pueden incluirse en una solicitud de pull request separada, donde puedan ser revisadas y probadas por sus méritos propios. Eso no implica que siempre se deban crear solicitudes de pull request diminutas; algunos trabajos arquitectónicos son necesariamente de alcance amplio. La cuestión es si cada edición significativa contribuye a un resultado coherente. Cuando la diferencia entre versiones es mayor que el problema en sí, por lo general se abordan varias cuestiones sin separarlas, lo que dificulta rastrear la causa y el efecto.

7. Código que funciona pero se resiste a la verificación

Algunas implementaciones se escriben rápidamente precisamente porque su comportamiento es difícil de observar o controlar. Una función lee directamente el reloj del sistema, construye sus propias dependencias, muta el estado global, realiza varias llamadas externas, ignora sus propios errores y casi no devuelve información alguna. El caso de éxito puede estar bien, pero demostrar cómo falla resulta costoso.

La probabilidad de pruebas tiende a reflejar la calidad de los límites definidos. Si una regla de negocio no puede aplicarse sin iniciar toda la aplicación, cargar filas en la base de datos, llamar a servicios reales y esperar a temporizadores reales, es probable que dicha regla esté vinculada a demasiados efectos secundarios.

Los flujos de trabajo asíncronos exponen esa brecha

El problema es más acusado en flujos asíncronos de múltiples pasos. Imagine una función que guarda un cambio, emite un evento del dominio, notifica a un usuario y agrega una fila al registro de auditoría. Si el tercer paso falla, ¿puede una prueba determinar qué pasos ya se completaron? ¿Se puede forzar que una dependencia agote su tiempo de espera? ¿Se puede simular que la función se ejecute dos veces? ¿Se puede reproducir una actualización obsoleta sin depender de la suerte con el tiempo?

El código que es fácil de escribir pero difícil de verificar fomenta la confianza en las pruebas manuales. Alguien sigue el camino exitoso, ve el resultado correcto y concluye que la función está lista. Luego, en entorno de producción, aparecen concurrencias, intentos de reintentar operaciones, datos inesperados y operaciones a medio completar que nadie probó localmente.

Verificar en el nivel donde reside el riesgo

La pureza en todo lugar y las burlas por todo tampoco son la solución. Las pruebas basadas principalmente en simulaciones tienden a congelar los detalles internos sin considerar cómo se comportan realmente la base de datos, la red o las transacciones. El enfoque mejor es decidir qué garantías son importantes y verificar cada una allí donde realmente radica su riesgo:

  • Las transformaciones puras pueden probarse de forma unitaria de manera aislada.
  • Las invariantes de la base de datos suelen requerir pruebas de integración con una base de datos real.
  • Los límites entre servicios pueden necesitar pruebas de contrato.
  • El comportamiento distribuido puede depender de registros estructurados e IDs de correlación, ya que no todas las condiciones de producción pueden reproducirse con fidelidad.

Inyectar un reloj o una dependencia, en lugar de recurrir a variables globales, suele ser el cambio más pequeño que permite hacer controlables las decisiones importantes. El problema no radica en una baja cobertura en sí misma, sino en códigos cuyas decisiones clave no pueden controlarse, observarse ni explicarse una vez que el camino óptimo deja de funcionar.

Puntos clave

Ninguno de estos indicadores constituye un veredicto automático. Una función larga puede representar una sola operación clara, una opción booleana puede ser completamente obvia, un helper compartido puede reflejar un comportamiento verdaderamente estable, y un cambio amplio podría ser inevitable. Considere esta lista como una guía para decidir dónde profundizar, no como una lista de verificación que deba cumplirse mecánicamente.

Al revisar el código, las preguntas fundamentales son:

  • ¿Están mezcladas las responsabilidades, de modo que un cambio arrastra consigo decisiones no relacionadas?
  • ¿Pueden los usuarios que llaman a la función entenderla sin tener que decodificar indicadores posicionales?
  • ¿Cada regla de negocio tiene un responsable claro?
  • ¿Una abstracción compartida oculta las diferencias reales detrás de las opciones?
  • ¿El manejo de errores mantiene la información precisa sobre qué falló?
  • ¿La diferencia es proporcional al requisito que satisface?
  • ¿Se puede verificar el comportamiento importante cuando surgen problemas?

Lo que realmente detectan los revisores experimentados es la incertidumbre diferida: la posibilidad de que el siguiente requisito obligue a alguien a comprender una lógica no relacionada, de que el próximo incidente deje poca evidencia para investigar, de que el siguiente desarrollador no sepa qué comportamiento fue intencional, o de que el próximo usuario solicite otra opción más a una abstracción que ya carga con demasiado. Reconocen rápidamente estos problemas porque ya han tenido que lidiar con ellos, desenredando ayudantes sobrecargados, rastreando excepciones no manejadas, reconciliando reglas contradictorias y revisando esa función “pequeña” que afectó a la mitad del código. El objetivo no es eliminar código que parezca desordenado, sino detectar aquel que hará el trabajo futuro más difícil de lo necesario.

Lecturas relacionadas