Pruebas y calidad de código

[Mal olor de código #1] ¿Por qué el código espagueti es espagueti?

El código espagueti no se refiere a la longitud ni al desorden, sino al flujo de control. Partiendo de la carta de Dijkstra contra goto de 1968, resumimos por qué el espagueti sobrevivió a la desaparición de goto y cómo medirlo con la complejidad ciclomática.

6 min de lectura
Imagen de portada de [Mal olor de código #1] ¿Por qué el código espagueti es espagueti?

Cuando el código es un desastre, los desarrolladores dicen que parece espagueti. Pero ¿qué es exactamente el espagueti?

En Mal olor de código #2, el próximo artículo previsto, continuaremos con el siguiente paquete.

¿Porque es largo? ¿Porque está desordenado?

Ninguna de las dos. Espagueti se refiere al flujo de control.

Es el estado en que el flujo se enreda como fideos al intentar seguir el orden de ejecución, de modo que no sabes hacia dónde saltará.

El proceso de separar código cuyos flujos de control y responsabilidades están mezclados en un solo archivo continúa en Caso de aplicación de separación de responsabilidades a un componente de 800 líneas.

Si seguimos la raíz del término, llegamos a una carta publicada en 1968.

Una carta escrita por Dijkstra

En marzo de 1968, Edsger Dijkstra publicó un texto breve en la revista de la ACM (Association for Computing Machinery, Asociación de Maquinaria Informática) (Artículo original).

El título era «Go To Statement Considered Harmful». Era una carta de dos páginas.

El título original de Dijkstra era «A Case Against the Go To Statement». El provocador título actual lo cambió el editor de entonces, Niklaus Wirth.

Este formato acabó convirtiéndose en la expresión «○○ Considered Harmful», así que la influencia del editor llegó bastante lejos.

La idea central es esta: las personas leen código estático y reconstruyen mentalmente el proceso dinámico de ejecución.

Pero en código con muchos goto, saber solo que «se está ejecutando esta línea» no explica el estado del programa. No sabes de dónde se llegó.

El código que usa solo ejecución secuencial, bifurcaciones condicionales y bucles es diferente.

Puedes describir la posición de ejecución como coordenadas: «tercera iteración del bucle externo, rama verdadera del if interno».

goto destruye este sistema de coordenadas.

Esta afirmación también tenía respaldo teórico. Dos años antes, en 1966, Corrado Böhm y Giuseppe Jacopini demostraron que cualquier programa puede expresarse solo con secuencia, selección y repetición (Artículo).

La matemática garantizaba así que era posible incluso sin goto.

El dibujo que creó goto

Basta con ver el aspecto del código de entonces para hacerse una idea.

10 IF X > 100 THEN GOTO 70
20 IF X < 0 THEN GOTO 90
30 Y = X * 2
40 IF Y > 50 THEN GOTO 70
50 PRINT Y
60 GOTO 100
70 PRINT "TOO BIG"
80 GOTO 100
90 PRINT "NEGATIVE"
100 END

Incluso con diez líneas había que dibujar el flujo en papel para entenderlo. Cada GOTO era una línea y esas líneas se cruzaban arriba y abajo.

No es difícil imaginar qué ocurre al llegar a 500 líneas.

Así eran realmente los códigos BASIC y Fortran iniciales de las décadas de 1970 y 1980; espagueti fue el nombre que recibió aquel amasijo.

Diagrama comparativo entre un flujo enredado con saltos GOTO y un flujo estructurado que usa solo secuencia y bifurcaciones
A la izquierda la posición de ejecución puede expresarse como coordenadas; a la derecha, no

goto desapareció, pero el espagueti permaneció

Aquí ocurre algo extraño. El código moderno casi no contiene goto.

Swift ni siquiera lo incluye. En los lenguajes que sí lo tienen, su uso se ha reducido a modismos como volver a puntos de limpieza de recursos.

Sin embargo, la expresión código espagueti se usa cada vez más.

goto era una de las causas, no la causa principal.

El problema real es que el lector no puede predecir el flujo, y el código moderno ofrece muchas otras formas de producirlo.

Infierno de anidamiento. Un if dentro de otro if, un closure dentro de este y otro if dentro del closure.

Como la indentación se profundiza con forma de flecha, se llama pirámide de la perdición. No salta como goto, pero rastrear a qué línea se llega con cada combinación de condiciones es igual de difícil.

Infierno de callbacks. Encadenar tareas asíncronas mediante callbacks hace que el orden de ejecución y el del código dejen de coincidir.

Cuando leer de arriba abajo deja de ser el orden de ejecución, el sistema de coordenadas vuelve a desmoronarse.

// No se puede leer el orden de ejecución como orden del código
loadUser(id) { user in
    loadProfile(user) { profile in
        loadPosts(profile) { posts in
            DispatchQueue.main.async {
                self.render(posts)   // ¿En qué capa debe gestionarse el error??
            }
        }
    }
}

Estado global. Si cualquier función puede cambiar una variable en cualquier momento, tienes que revisar todo el proyecto para rastrear por qué su valor terminó así.

Por eso los singletons también reciben críticas con frecuencia.

Sopa de eventos. A envía una notificación, B la recibe y cambia el estado, y C, que lo observaba, envía otra notificación.

Cada pieza es breve y limpia, pero el flujo general no está documentado en ninguna parte.

Es una forma habitual del código reactivo y, en cierto sentido, más difícil de rastrear que goto, porque el destino del salto no aparece en el código.

Imagen de un grafo de flujo de control con rutas independientes de la complejidad ciclomática y un indicador de umbral
Los números de complejidad no son la respuesta, sino una señal que indica dónde mirar

¿Se puede medir con un número?

«Este código parece espagueti» es una afirmación subjetiva. Por eso, en 1976, Thomas McCabe propuso la métrica Complejidad Ciclomática (Artículo).

El cálculo es sencillo: dibuja el flujo de control del código como un grafo y cuenta cuántas rutas independientes hay.

En la práctica se aproxima sumando 1 al número de puntos de bifurcación. Cada if, for, while, case, && y ?? suma aproximadamente 1.

La convención habitual considera que, por encima de 10, es hora de dividir la función. El propio McCabe presentó esa cifra como un límite superior razonable, no como un criterio absoluto.

En realidad, una función que enumera 20 casos con un solo switch puede superar una complejidad de 20 y seguir siendo fácil de leer.

La regla cyclomatic_complexity de SwiftLint mide un valor similar.

Pero SwiftLint solo cuenta if, guard, for, while, repeat, case y catch; no cuenta && ni ??. El umbral de advertencia predeterminado es 10.

Si la activas en el proyecto, una función que crecía silenciosamente acabará siendo detectada.

La réplica de Knuth

Terminar esta historia con «Dijkstra ganó» significa conocer solo la mitad.

En 1974, Donald Knuth presentó una réplica en un extenso artículo titulado «Structured Programming with go to Statements» (Artículo).

La idea era que goto en sí no es malo. Hay situaciones, como salir de varios bucles anidados de una vez, en las que usar goto hace el flujo más claro.

Intentar eliminarlo creando una variable bandera booleana y añadiendo condicionales produce un código peor.

La conclusión de los lenguajes actuales se acerca a un compromiso: eliminan los saltos ilimitados, pero ofrecen sintaxis específica para los patrones frecuentes.

break, continue, break label con etiqueta, y defer y guard de Swift son ejemplos.

// guard: Saca las excepciones al principio y mantén plano el cuerpo principal
func process(_ data: Data?) throws -> Packet {
    guard let data else { throw ParseError.empty }
    guard data.count > headerSize else { throw ParseError.tooShort }
    // A partir de aquí, todas las condiciones ya están resueltas
    return try decode(data)
}

guard se parece a que el patrón goto cleanup de la época de C se consolidara como una función del lenguaje. Fija el destino del salto en un único lugar y le da un nombre.

Resumen

  • El código espagueti no es código desordenado, sino código cuyo flujo de control no se puede predecir.
  • La carta de Dijkstra de 1968 fue el punto de partida, y su argumento central era que «la posición de ejecución debe poder describirse como coordenadas».
  • gotodesapareció, pero el anidamiento profundo, los callbacks anidados, el estado global y las cadenas de eventos recrean el mismo problema.
  • Puede medirse aproximadamente con la complejidad ciclomática; alrededor de 10 es una línea de advertencia habitual.
  • Como sostuvo Knuth, el objetivo no es erradicar goto, sino conseguir un flujo predecible. guard y async/await son mecanismos con los que el lenguaje ayuda a proteger ese objetivo.

En el próximo artículo veremos código roto en la dirección opuesta. El flujo es perfectamente ordenado, pero añadir un valor obliga a modificar siete archivos.

Es código lasaña.

Seguir leyendo

Fuentes y verificación

  • Go To Statement Considered HarmfulEdsger W. Dijkstra Archive · Fuente original del autor · Consultado 17 de agosto de 2026Respalda: La crítica de goto de 1968 y el problema de inferir el flujo de control
  • Flow Diagrams, Turing Machines and LanguagesCorrado Böhm·Giuseppe Jacopini · Artículo de investigación · Consultado 17 de agosto de 2026Respalda: La expresividad de las estructuras secuenciales, de selección y de repetición en 1966
  • A Complexity MeasureThomas J. McCabe · Artículo de investigación · Consultado 17 de agosto de 2026Respalda: La definición de complejidad ciclomática y los grafos de flujo de control de 1976
  • Structured Programming with go to StatementsDonald E. Knuth · Artículo de investigación · Consultado 17 de agosto de 2026Respalda: La programación estructurada y el papel limitado de goto en 1974