La programación estructurada es un paradigma de programación cuyo objetivo es mejorar la claridad, la calidad y el tiempo de desarrollo de un programa informático mediante el uso extensivo de las construcciones de flujo de control estructurado de selección ( if/then/else ) y repetición ( while y for ), estructuras de bloques y subrutinas .
Surgió a finales de los años 50 con la aparición de los lenguajes de programación ALGOL 58 y ALGOL 60 [1] , este último con soporte para estructuras de bloques. Entre los factores que contribuyeron a su popularidad y amplia aceptación, primero en el ámbito académico y más tarde entre los profesionales, se incluyen el descubrimiento de lo que ahora se conoce como el teorema del programa estructurado en 1966 [2] y la publicación de la influyente carta abierta " Go To Statement Considered Harmful " en 1968 del informático holandés Edsger W. Dijkstra , que acuñó el término "programación estructurada". [3]
La programación estructurada se utiliza con mayor frecuencia con desviaciones que permiten programas más claros en algunos casos particulares, como cuando se debe realizar el manejo de excepciones .
Siguiendo el teorema del programa estructurado , todos los programas se consideran compuestos de tres estructuras de control :
if..then..else..endif
. La sentencia 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 tenga solo un punto de entrada (y en la programación estructural original, también solo un punto de salida, y algunos lenguajes lo imponen).Subrutinas : unidades invocables como procedimientos, funciones, métodos o subprogramas que se utilizan para permitir que una sola declaración haga referencia a una secuencia.
Los bloques se utilizan para permitir que grupos de sentencias se traten como si fueran una sola sentencia. Los lenguajes estructurados en bloques tienen una sintaxis para encerrar estructuras de alguna manera formal, como una sentencia if entre corchetes como if..fi
en ALGOL 68 , o una sección de código entre corchetes BEGIN..END
como en PL/I y Pascal , sangría de espacios en blanco como en Python , o las llaves {...}
de C y muchos lenguajes posteriores .
Es posible realizar programación estructurada en cualquier lenguaje de programación, aunque es preferible utilizar algo así como un lenguaje de programación procedimental . [4] [5] Algunos de los lenguajes utilizados inicialmente para la programación estructurada incluyen: ALGOL , Pascal , PL/I , Ada y RPL, pero la mayoría de los nuevos lenguajes de programación procedimental desde entonces han incluido características para fomentar la programación estructurada y, a veces, han omitido deliberadamente características, en particular GOTO , en un esfuerzo por hacer que la programación no estructurada sea más difícil.
La programación estructurada (a veces conocida como programación modular [4] ) impone una estructura lógica al programa que se está escribiendo para hacerlo más eficiente y más fácil de entender y modificar.
El teorema del programa estructurado proporciona la base teórica de la programación estructurada. Afirma que tres formas de combinar programas (secuenciación, selección e iteración) son suficientes para expresar cualquier función computable . Esta observación no se originó con el movimiento de 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 generalmente atribuyen el resultado a un artículo de 1966 de Böhm y Jacopini, posiblemente porque Dijkstra citó este artículo él mismo. [6] El teorema del programa estructurado no aborda cómo escribir y analizar un programa estructurado útil. Estas cuestiones se abordaron a fines de la década de 1960 y principios de la de 1970, con importantes contribuciones de Dijkstra , Robert W. Floyd , Tony Hoare , Ole-Johan Dahl y David Gries .
PJ Plauger , uno de los primeros en adoptar la programación estructurada, describió su reacción al teorema del programa estructurado:
Nosotros, los conversos, agitamos esta interesante noticia ante las narices de los programadores de lenguaje ensamblador no recalcitrantes que no paraban de sacar a relucir retorcidos fragmentos de lógica y decir: "Apuesto a que no puedes estructurar esto". Ni la demostración de Böhm y Jacopini ni nuestros repetidos éxitos escribiendo código estructurado los hicieron cambiar de opinión un día antes de lo que estaban dispuestos a convencerse a sí mismos. [7]
Donald Knuth aceptó el principio de que los programas deben escribirse teniendo en cuenta la demostrabilidad, pero no estuvo de acuerdo con la abolición de la sentencia GOTO y, a partir de 2018, [actualizar]ha seguido utilizándola en sus programas. [8] En su artículo de 1974, "Programación estructurada con sentencias Goto", [9] dio ejemplos en los que creía que un salto directo conduce a un código más claro y eficiente sin sacrificar la demostrabilidad. Knuth propuso una restricción estructural más flexible: debería ser posible dibujar el diagrama de flujo de un programa con todas las ramas hacia adelante a la izquierda, todas las ramas hacia atrás a la derecha y ninguna rama que se cruce entre sí. Muchos de los expertos en compiladores y teoría de grafos han abogado por permitir solo grafos de flujo reducibles . [ ¿Cuándo se define como? ] [ ¿ Quién? ]
Los teóricos de la programación estructurada ganaron un aliado importante en la década de 1970, cuando el investigador de IBM Harlan Mills aplicó su interpretación de la teoría de la programación estructurada al desarrollo de un sistema de indexación para el archivo de investigación de The New York Times . El proyecto fue un gran éxito de ingeniería y los gerentes de otras empresas lo citaron como apoyo a la adopción de la programación estructurada, aunque Dijkstra criticó las formas en que la interpretación de Mills difería del trabajo publicado. [10]
En 1987 todavía era posible plantear la cuestión de la programación estructurada en una revista de informática. Frank Rubin lo hizo ese año con una carta abierta titulada «'GOTO Considered Harmful' Considered Harmful» [11] . Siguieron numerosas objeciones, incluida una respuesta de Dijkstra que criticaba duramente tanto a Rubin como a las concesiones que otros escritores hicieron al responderle.
A finales del siglo XX, casi todos los informáticos estaban convencidos de que resulta ú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.
Aunque goto ha sido reemplazado 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, que se encuentra en muchos lenguajes, es el uso de una sentencia return para la salida temprana de una subrutina. Esto da como resultado 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 difíciles en la programación puramente estructurada.
La desviación más común de la programación estructurada es la salida anticipada de una función o bucle. A nivel de funciones, se trata de una return
declaración. A nivel de bucles, se trata de una break
declaración (terminar el bucle) o continue
una declaración (terminar la iteración actual, continuar con la siguiente iteración). En la programación estructurada, se pueden replicar añadiendo ramas o pruebas adicionales, pero para los retornos de código anidado esto puede añadir una complejidad significativa. C es un ejemplo temprano y destacado de estas construcciones. Algunos lenguajes más nuevos también tienen "rupturas etiquetadas", que permiten salir de más que solo el bucle más interno. Las excepciones también permiten la salida anticipada, pero tienen más consecuencias, y por ello se tratan a continuación.
Pueden surgir salidas múltiples por una variedad de razones, la mayoría de las veces porque la subrutina ya no tiene más trabajo que hacer (si devuelve un valor, ha completado el cálculo) o ha encontrado circunstancias "excepcionales" que le impiden continuar, por lo que necesita un manejo de excepciones.
El problema más común en la salida temprana es que no se ejecutan las declaraciones de limpieza o finales; por ejemplo, no se desasigna la memoria asignada o no se cierran los archivos abiertos, lo que provoca fugas de memoria o fugas de recursos . Esto se debe hacer en cada sitio de retorno, lo que es frágil y puede generar errores fácilmente. Por ejemplo, en un desarrollo posterior, un desarrollador podría pasar por alto una declaración de retorno y una acción que debería realizarse al final de una subrutina (por ejemplo, una declaración de seguimiento ) podría no realizarse en todos los casos. Los lenguajes sin una declaración de retorno, como Pascal estándar y Seed7 , no tienen este problema.
La mayoría de los lenguajes modernos proporcionan soporte a nivel de lenguaje para evitar tales fugas; [12] vea una discusión detallada en administración de recursos . Lo más común es que esto se haga a través de la protección de desenrollado, que asegura que se garantice que cierto código se ejecutará cuando la ejecución salga de un bloque; esta es una alternativa estructurada a tener un bloque de limpieza y un goto
. Esto se conoce más a menudo como try...finally,
y se considera una parte del manejo de excepciones . En caso de múltiples return
declaraciones, la introducción try...finally,
sin excepciones puede parecer extraña. Existen varias técnicas para encapsular la administración de recursos. Un enfoque alternativo, que se encuentra principalmente en C++, es Resource Acquisition Is Initialization , que utiliza el desenrollado de pila normal (liberación de variables) en la salida de la función para llamar a los destructores en las variables locales para desasignar recursos.
Kent Beck , Martin Fowler y coautores han argumentado en sus libros de refactorización que los condicionales anidados pueden ser más difíciles de entender que un cierto tipo de estructura más plana que utiliza múltiples salidas predicadas por cláusulas de protección . Su libro de 2009 afirma 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, use un punto de salida; de lo contrario, no lo haga". Ofrecen una solución de libro de cocina para transformar una función que consiste solo en condicionales anidados en una secuencia de declaraciones de retorno (o lanzamiento) protegidas, seguidas de un solo bloque sin protección, que está destinado a contener el código para el caso común, mientras que las declaraciones protegidas se supone que tratan con las menos comunes (o con errores). [13] 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. [14]
En su libro de texto de 2004, David Watt escribe que "los flujos de control de entrada única y salida múltiple suelen ser deseables". Utilizando la noción de marco de Tennent de secuenciador , Watt describe de manera uniforme las construcciones de flujo de control que se encuentran en los lenguajes de programación contemporáneos e intenta explicar por qué ciertos tipos de secuenciadores son preferibles a otros en el contexto de flujos de control de salida múltiple. Watt escribe que los gotos sin restricciones (secuenciadores de salto) son malos porque el destino del salto no se explica por sí mismo 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. Por el contrario, Watt argumenta que la intención conceptual de un secuenciador de retorno es clara a partir de su propio contexto, sin tener que examinar su destino. Watt escribe que una clase de secuenciadores conocidos como secuenciadores de escape , definidos como un "secuenciador que termina la ejecución de un comando o procedimiento que encierra texto", abarca tanto las interrupciones de bucles (incluidas las interrupciones de varios niveles) como las declaraciones de retorno. Watt también señala que, si bien los secuenciadores de salto (gotos) han sido algo restringidos en lenguajes como C, donde el objetivo debe ser un bloque interno local o un bloque externo que lo abarque, esa restricción por sí sola no es suficiente para que la intención de los gotos en C sea autodescriptiva y, por lo tanto, aún puedan producir " código espagueti ". Watt también examina en qué se diferencian los secuenciadores de excepción de los secuenciadores de escape y de salto; esto se explica en la siguiente sección de este artículo. [15]
En contraste con lo anterior, Bertrand Meyer escribió en su libro de texto de 2009 que instrucciones como break
y continue
"son simplemente viejas goto
palabras con piel de oveja" y desaconsejó firmemente su uso. [16]
Basándose en el error de codificación del desastre del Ariane 501 , el desarrollador de software Jim Bonang sostiene que cualquier excepción lanzada desde una función viola el paradigma de salida única y propone que se prohíban todas las excepciones entre procedimientos. Bonang propone que todo C++ que cumpla con el paradigma de salida única se escriba siguiendo los siguientes lineamientos:
bool MyCheck1 () throw () { bool success = false ; try { // Hacer algo que pueda generar excepciones. if ( ! MyCheck2 ()) { throw SomeInternalException (); } // Otro código similar al anterior. success = true ; } catch (...) { // Todas las excepciones capturadas y registradas. } return success ; }
Peter Ritchie también señala que, en principio, incluso un solo throw
right antes de return
una función constituye una violación del principio de salida única, pero argumenta que las reglas de Dijkstra fueron escritas en una época anterior a que el manejo de excepciones se convirtiera en un paradigma en los lenguajes de programación, por lo que propone permitir cualquier número de puntos de lanzamiento además de un único punto de retorno. Señala que las soluciones que envuelven excepciones con el fin de crear una salida única tienen una mayor profundidad de anidamiento y, por lo tanto, son más difíciles de comprender, e incluso acusa a quienes proponen aplicar tales soluciones a lenguajes de programación que admiten excepciones de participar en el pensamiento de culto a la carga . [17]
David Watt también analiza el manejo de excepciones en el marco de los secuenciadores (introducido en este artículo en la sección anterior sobre salidas tempranas). Watt señala que una situación anormal (generalmente ejemplificada con desbordamientos aritméticos o fallas de entrada/salida como archivo no encontrado) es un tipo de error que "se detecta en alguna unidad de programa de bajo nivel, pero [para el cual] un manejador se ubica de manera más natural en una unidad de programa de alto nivel". Por ejemplo, un programa puede contener varias llamadas para leer archivos, pero la acción a realizar cuando no se encuentra un archivo depende del significado (propósito) del archivo en cuestión para el programa y, por lo tanto, una rutina de manejo para esta situación anormal no se puede ubicar en el código de sistema de bajo nivel. Watts señala además que la introducción de pruebas de indicadores de estado en el llamador, como implicaría la programación estructurada de salida única o incluso los secuenciadores de retorno (de múltiples salidas), da como resultado una situación en la que "el código de la aplicación tiende a verse abarrotado de pruebas de indicadores de estado" y que "el programador podría omitir, por olvido o por pereza, probar un indicador de estado. De hecho, las situaciones anormales representadas por indicadores de estado se ignoran por defecto". Señala que, en contraste con las pruebas de indicadores de estado, las excepciones tienen el comportamiento predeterminado opuesto , lo que hace que el programa finalice a menos que el programador trate explícitamente la excepción de alguna manera, posiblemente agregando código para ignorarla voluntariamente. Basándose en estos argumentos, Watt concluye que los secuenciadores de salto o los secuenciadores de escape (discutidos en la sección anterior) no son tan adecuados como un secuenciador de excepciones dedicado con la semántica discutida anteriormente. [18]
El libro de texto de Louden y Lambert enfatiza que el manejo de excepciones difiere de las construcciones de programación estructurada como while
los bucles porque la transferencia de control "se establece en un punto diferente en el programa que aquel donde la transferencia real tiene lugar. En el punto donde la transferencia realmente ocurre, puede que no haya ninguna indicación sintáctica de que el control de hecho será transferido". [19] El profesor de ciencias de la computación Arvind Kumar Bansal también señala que en lenguajes que implementan el manejo de excepciones, incluso estructuras de control como for
, que tienen la propiedad de salida única en ausencia de excepciones, ya no la tienen en presencia de excepciones, porque una excepción puede causar prematuramente una salida temprana en cualquier parte de la estructura de control; por ejemplo, si init()
lanza una excepción en for (init(); check(); increm())
, entonces no se alcanza el punto de salida habitual después de check(). [20] Citando múltiples estudios previos de otros (1999-2004) y sus propios resultados, Westley Weimer y George Necula escribieron que un problema significativo con las excepciones es que "crean rutas de flujo de control ocultas que son difíciles de razonar para los programadores". [21]
La necesidad de limitar el código a puntos de salida únicos aparece en algunos entornos de programación contemporáneos centrados en la computación paralela , como OpenMP . Las diversas construcciones paralelas de OpenMP, como parallel do
, no permiten salidas tempranas desde dentro hacia fuera de la construcción paralela; esta restricción incluye todo tipo de salidas, desde break
hasta excepciones de C++, pero todas ellas están permitidas dentro de la construcción paralela si el objetivo de salto también está dentro de ella. [22]
En casos más raros, los subprogramas permiten múltiples entradas. Esto es más comúnmente solo el reingreso a una corrutina (o generador /semicorutina), donde un subprograma cede el control (y posiblemente un valor), pero luego puede reanudarse donde se dejó. Hay una serie de usos comunes de este tipo de programación, en particular para flujos (particularmente entrada/salida), máquinas de estado y concurrencia. Desde el punto de vista de la ejecución de código, ceder desde una corrutina es más cercano a la programación estructurada que regresar desde una subrutina, ya que el subprograma en realidad no ha terminado y continuará cuando se lo llame nuevamente; no es una salida anticipada. Sin embargo, las corrutinas significan que múltiples subprogramas tienen estado de ejecución, en lugar de una única pila de llamadas de subrutinas, y por lo tanto introducen una forma diferente de complejidad.
Es muy raro que los subprogramas permitan la entrada a una posición arbitraria en el subprograma, ya que en este caso el estado del programa (como los valores de las variables) no está inicializado o es ambiguo, y esto es muy similar a un goto.
Algunos programas, en particular los analizadores sintácticos y los protocolos de comunicación , tienen una serie de estados que se suceden entre sí de una manera que no se puede reducir fácilmente a las estructuras básicas, y algunos programadores implementan los cambios de estado con un salto al nuevo estado. Este tipo de cambio de estado se utiliza a menudo en el núcleo de Linux. [ cita requerida ]
Sin embargo, es posible estructurar estos sistemas convirtiendo cada cambio de estado en un subprograma independiente y utilizando una variable para indicar el estado activo (véase trampolín ). Alternativamente, estos pueden implementarse mediante corrutinas, que prescinden del trampolín.
{{cite book}}
: CS1 maint: varios nombres: lista de autores ( enlace )Ejemplo 4: Una sola entrada, una sola salida ("SESE"). Históricamente, algunos estándares de codificación exigían que cada función tuviera exactamente una salida, es decir, una declaración de retorno. Este requisito está obsoleto en lenguajes que admiten excepciones y destructores, donde las funciones suelen tener numerosas salidas implícitas.