La programación estructurada es un paradigma de programación orientado a mejorar la claridad, calidad y tiempo de desarrollo de un programa de computadora recurriendo únicamente a subrutinas y a tres estructuras de control básicas: secuencia, selección (if y switch) e iteración (bucles for y while); asimismo, se considera innecesario y contraproducente el uso de la transferencia incondicional (GOTO); esta instrucción suele acabar generando el llamado código espagueti, mucho más difícil de seguir y de mantener, además de originar numerosos errores de programación.
Surgió en la década de 1960, particularmente del trabajo de Corrado Böhm y Giuseppe Jacopini,[1] y un famoso escrito de 1968: «La sentencia goto, considerada perjudicial», de Edsger Dijkstra.[2] Sus postulados se verían reforzados, a nivel teórico, por el teorema del programa estructurado y, a nivel práctico, por la aparición de lenguajes como ALGOL, dotado de estructuras de control consistentes y bien formadas.[3]
Siguiendo el teorema del programa estructurado, todos los programas se ven como compuestos de estructuras de control:
if..then..else..endif
. La declaración condicional debe tener al menos una condición verdadera y cada condición debe tener un punto de salida como máximo.while
, repeat
, for
o do..until
. A menudo, se recomienda que cada bucle solo tenga un punto de entrada (y en la programación estructural original, también solo un punto de salida, y algunos lenguajes lo imponen).Subrutinas son las unidades a las que se puede llamar, como procedimientos, funciones, métodos o subprogramas. Se utilizan para permitir que una sola declaración haga referencia a una secuencia.
Los bloques se utilizan para permitir que grupos de declaraciones se traten como si fueran una sola declaración. Los lenguajes "estructurados en bloques" tienen una sintaxis para encerrar estructuras de alguna manera formal, como una declaración if entre paréntesis. if..fi
como en ALGOL 68, o una sección de código entre corchetes BEGIN..END
, como en PL/I y Pascal, sangría de espacio en blanco como en Python, o las llaves {...}
de C y relacionados de muchos lenguajes posteriores.
A finales de los años 1970 surgió una nueva forma de programar que no solamente permitía desarrollar programas fiables y eficientes, sino que además estos estaban escritos de manera que se facilitaba su comprensión en fases de mejora posteriores.
El teorema del programa estructurado, propuesto por Böhm-Jacopini, demuestra que todo programa puede escribirse utilizando únicamente las tres instrucciones de control siguientes:
Solamente con estas tres estructuras se pueden escribir todos los programas y aplicaciones posibles. Si bien los lenguajes de programación tienen un mayor repertorio de estructuras de control, estas pueden ser construidas mediante las tres básicas citadas.
El teorema del programa estructurado proporciona la base teórica de la programación estructurada. Señala que la combinación de las tres estructuras básicas, secuencia, selección e iteración, son suficientes para expresar cualquier función computable. Esta observación no se originó con el movimiento de la programación estructurada. Estas estructuras son suficientes para describir el ciclo de instrucción de una unidad central de procesamiento, así como el funcionamiento de una máquina de Turing. Por lo tanto, un procesador siempre está ejecutando un «programa estructurado» en este sentido, incluso si las instrucciones que lee de la memoria no son parte de un programa estructurado. Sin embargo, los autores usualmente acreditan el resultado a un documento escrito en 1966 por Böhm y Jacopini, posiblemente porque Dijkstra había citado este escrito.[4] El teorema del programa estructurado no responde a cómo escribir y analizar un programa estructurado de manera útil. Estos temas fueron abordados durante la década de 1960 y principio de los años 1970, con importantes contribuciones de Dijkstra, Robert W. Floyd, Tony Hoarey y David Gries.
El programador norteamericano P. J. Plauger (1944- ), uno de los primeros en adoptar la programación estructurada, describió su reacción con el teorema del programa estructurado:
Nosotros los conversos ondeamos esta interesante pizca de noticias bajo las narices de los recalcitrantes programadores de lenguaje ensamblador que mantuvieron trotando adelante retorcidos bits de lógica y diciendo, 'Te apuesto que no puedes estructurar esto'. Ni la prueba por Böhm y Jacopini, ni nuestros repetidos éxitos en escribir código estructurado, los llevaron un día antes de lo que estaban listos para convencerse.[5]
Donald Knuth aceptó el principio de que los programas deben adaptarse con asertividad, pero no estaba de acuerdo (y aún está en desacuerdo)[cita requerida] con la supresión de la sentencia GOTO. En su escrito de 1974 «Programación estructurada con sentencias Goto», dio ejemplos donde creía que un salto directo conduce a código más claro y más eficiente sin sacrificar demostratividad. Knuth propuso una restricción estructural más flexible: debe ser posible establecer un diagrama de flujo del programa con todas las bifurcaciones hacia adelante a la izquierda, todas las bifurcaciones hacia atrás a la derecha, y sin bifurcaciones que se crucen entre sí. Muchos de los expertos en teoría de grafos y compiladores han abogado por permitir solo grafos de flujo reducible[¿quién?][¿cuándo?].
Los teóricos de la programación estructurada se ganaron un aliado importante en la década de 1970 después de que el investigador de IBM Harlan Mills aplicara su interpretación de la teoría de la programación estructurada para el desarrollo de un sistema de indexación para el archivo de investigación del New York Times. El proyecto fue un gran éxito de la ingeniería, y los directivos de otras empresas lo citaron en apoyo de la adopción de la programación estructurada, aunque Dijkstra criticó las maneras en que la interpretación de Mills difería de la obra publicada.[6]
Habría que esperar a 1987 para que la cuestión de la programación estructurada llamara la atención de una revista de ciencia de la computación. Frank Rubin lo hizo en ese año, con el escrito: «¿“La sentencia GOTO considerada dañina” se considera dañina?».[7] A este le siguieron numerosas objeciones, como una respuesta del propio Dijkstra que criticaba duramente a Rubin y las concesiones que otros autores hicieron cuando le respondieron.
A finales del siglo XX, casi todos los científicos están convencidos de que es útil aprender y aplicar los conceptos de programación estructurada. Los lenguajes de programación de alto nivel que originalmente carecían de estructuras de programación, como FORTRAN, COBOL y BASIC, ahora las tienen.
Entre las ventajas de la programación estructurada sobre el modelo anterior (hoy llamado despectivamente código espagueti), cabe citar las siguientes:
Si bien es posible desarrollar la programación estructurada en cualquier lenguaje de programación, resulta más idóneo un lenguaje de programación procedimental. Algunos de los lenguajes utilizados inicialmente para programación estructurada incluyen ALGOL, Pascal, PL/I y Ada, pero la mayoría de los nuevos lenguajes de programación procedimentales desde entonces han incluido características para fomentar la programación estructurada y a veces, deliberadamente, omiten características[8] en un esfuerzo para hacer más difícil la programación no estructurada.
Con posterioridad a la programación estructurada se han creado nuevos paradigmas tales como la programación modular, la programación orientada a objetos, la programación por capas y otras, así como nuevos entornos de programación que facilitan la programación de grandes aplicaciones y sistemas.
Aunque goto ha sido sustituido en gran medida por las construcciones estructuradas de selección (if/then/else) y repetición (while y for), pocos lenguajes son puramente estructurados. La desviación más común, encontrada en muchos lenguajes, es el uso de una sentencia return para la salida temprana de una subrutina. Esto resulta en múltiples puntos de salida, en lugar del único punto de salida requerido por la programación estructurada. Hay otras construcciones para manejar casos que son incómodos en la programación puramente estructurada.
La desviación más común de la programación estructurada es la salida temprana de una función o bucle. A nivel de funciones, esto es una declaración de return
. A nivel de bucles, esto es una declaración de break
(terminar el bucle) o una declaración de continue
(terminar la iteración actual y proceder con la siguiente). En la programación estructurada, estos se pueden replicar añadiendo ramas o pruebas adicionales, pero para retornos desde código anidado, esto puede agregar una complejidad significativa. C es un ejemplo temprano y prominente de estos constructos. Algunos lenguajes más nuevos también tienen «breaks etiquetados», que permiten salir de más que solo el bucle más interno. Las excepciones también permiten la salida temprana, pero tienen consecuencias adicionales, y por lo tanto se tratan a continuación.
Las salidas múltiples pueden surgir por una variedad de razones, la mayoría de las veces porque la subrutina no tiene más trabajo que hacer (si devuelve un valor, ha completado el cálculo), o se ha encontrado con circunstancias «excepcionales» que le impiden continuar, por lo que necesita un manejo de excepciones.
El problema más común en la salida anticipada es que las sentencias de limpieza o finales no se ejecutan - por ejemplo, la memoria asignada no se desasigna, o los archivos abiertos no se cierran, causando fugas de memorias o fugas de recursoss. Esto debe hacerse en cada lugar de retorno. Esto debe hacerse en cada sitio de retorno, lo que es frágil y puede dar lugar fácilmente a errores. Por ejemplo, en un desarrollo posterior, una sentencia return podría ser pasada por alto por un desarrollador, y una acción que debería realizarse al final de una subrutina (por ejemplo, una sentencia trace) podría no realizarse en todos los casos. Los lenguajes sin sentencia return, como Pascal estándar y Seed7, no tienen este problema.
La mayoría de los lenguajes modernos proporcionan soporte a nivel de lenguaje para prevenir tales fugas;[9] consulta la discusión detallada en gestión de recursos. Esto se hace comúnmente a través de la protección de desanudado, que asegura que cierto código se ejecute garantizado al salir de un bloque; esta es una alternativa estructurada a tener un bloque de limpieza y un goto
. Esto se conoce más frecuentemente como try...finally
, y se considera parte de manejadores de excepciones. En el caso de múltiples declaraciones de return
, introducir try...finally
sin excepciones puede parecer extraño. Existen varias técnicas para encapsular la gestión de recursos. Un enfoque alternativo, encontrado principalmente en C++, es Resource Acquisition Is Initialization, que utiliza el desanudado normal de la pila (desasignación de variables) al salir de la función para llamar a destructores en variables locales y desasignar recursos.
Kent Beck, Martin Fowler y coautores han argumentado en sus libros sobre refactorización que los condicionales anidados pueden ser más difíciles de entender que un cierto tipo de estructura más plana utilizando múltiples salidas predicadas por cláusulas de guardia. Su libro de 2009 establece rotundamente que "un punto de salida realmente no es una regla útil. La claridad es el principio clave: si el método es más claro con un punto de salida, utiliza un punto de salida; de lo contrario, no lo hagas". Ofrecen una solución tipo "recetario" para transformar una función que consiste solo en condicionales anidados en una secuencia de declaraciones de retorno (o lanzamiento) protegidas, seguida de un bloque sin protección, que está destinado a contener el código para el caso común, mientras que las declaraciones protegidas deben tratar con los casos menos comunes (o con errores).[10] Herb Sutter y Andrei Alexandrescu también argumentan en su libro de consejos de C++ de 2004 que el punto de salida único es un requisito obsoleto.[11]
En su libro de texto de 2004, David Watt escribe que "los flujos de control de entrada única y múltiples salidas son a menudo deseables". Usando la noción del marco de Tennent de secuenciador, Watt describe uniformemente los constructos de flujo de control encontrados en los lenguajes de programación contemporáneos y intenta explicar por qué ciertos tipos de secuenciadores son preferibles a otros en el contexto de flujos de control de múltiples salidas. Watt escribe que los gotos sin restricciones (secuenciadores de salto) son malos porque el destino del salto no es autoexplicativo para el lector de un programa hasta que el lector encuentra y examina la etiqueta o dirección real que es el objetivo del salto. En contraste, Watt argumenta que la intención conceptual de un secuenciador de retorno es clara desde su propio contexto, sin necesidad de examinar su destino. Watt escribe que una clase de secuenciadores conocidos como secuenciadores de escape, definidos como "secuenciador que termina la ejecución de un comando o procedimiento que lo envuelve textualmente", abarca tanto los saltos de bucles (incluyendo saltos de múltiples niveles) como las declaraciones de retorno. Watt también señala que, aunque los secuenciadores de salto (gotoss) han sido algo restringidos en lenguajes como C, donde el objetivo debe estar dentro del bloque local o de un bloque exterior que lo envuelva, esa restricción por sí sola no es suficiente para hacer que la intención de los gotos en C sea auto-descriptiva y, por lo tanto, aún pueden producir "código espagueti". Watt también examina cómo los secuenciadores de excepciones difieren de los secuenciadores de escape y de salto; esto se explica en la siguiente sección de este artículo.[12]
En contraste con lo anterior, Bertrand Meyer escribió en su libro de texto de 2009 que instrucciones como break
y continue
"son solo el viejo goto
disfrazado" y aconsejó enérgicamente en contra de su uso.[13]