Programación estructurada
Programación estructurada es un paradigma de programación destinado a mejorar la claridad, la calidad y el tiempo de desarrollo de un programa de computadora haciendo un uso extensivo de las construcciones de selección de flujo de control estructurado (if/then/else) y repetición (while y for), estructuras de bloques y subrutinas.
Surgió a fines de la década de 1950 con la aparición de los lenguajes de programación ALGOL 58 y ALGOL 60, y este último incluía soporte para estructuras de bloques. Los factores que contribuyeron a su popularidad y aceptación generalizada, al principio en la academia y luego entre los profesionales, incluyen el descubrimiento de lo que ahora se conoce como el teorema del programa estructurado en 1966, y la publicación de la influyente "Go To Statement Considered Harmful" 34; carta abierta en 1968 del científico informático holandés Edsger W. Dijkstra, quien acuñó el término "programación estructurada".
La programación estructurada se usa con mayor frecuencia con desviaciones que permiten programas más claros en algunos casos particulares, como cuando se debe realizar el manejo de excepciones.
Elementos
Estructuras de control
Siguiendo el teorema del programa estructurado, todos los programas se consideran compuestos por tres estructuras de control:
- "Secuencia"; ordenadas declaraciones o subrutinas ejecutadas en secuencia.
- "Selección"; una o varias declaraciones se ejecutan dependiendo del estado del programa. Esto se expresa generalmente con palabras clave como si.. entonces..else.endif. La declaración condicional debe tener al menos una condición verdadera y cada condición debe tener un punto de salida al máximo.
- "Iteración"; una declaración o bloque se ejecuta hasta que el programa llega a un determinado estado, o las operaciones se han aplicado a cada elemento de una colección. Esto se expresa generalmente con palabras clave como mientras, repetir, para o hacer..hasta. A menudo se recomienda que cada bucle sólo tenga un punto de entrada (y en la programación estructural original, también sólo un punto de salida, y algunos idiomas lo hagan).
Subrutinas
Subrutinas; 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.
Bloques
Los bloques se utilizan para permitir que los 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 corchetes por if..fi
como en ALGOL 68, o una sección de código entre paréntesis BEGIN..END
, como en PL/I y Pascal, sangría en blanco como en Python, o las llaves {...}
de C y muchos lenguajes posteriores.
Lenguajes de programación estructurados
Es posible hacer programación estructurada en cualquier lenguaje de programación, aunque es preferible usar algo como un lenguaje de programación procedimental. Algunos de los lenguajes que se usaron 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 funciones para fomentar la programación estructurada y, a veces, han omitido funciones deliberadamente, 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) impone una estructura lógica en el programa que se está escribiendo para hacerlo más eficiente y más fácil de entender y modificar.
Historia
Fundamento teórico
El teorema del programa estructurado proporciona la base teórica de la programación estructurada. Establece 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 instrucciones 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 forman parte de un programa estructurado. Sin embargo, los autores suelen atribuir el resultado a un artículo de 1966 de Böhm y Jacopini, posiblemente porque Dijkstra mismo citó este artículo. El teorema del programa estructurado no aborda cómo escribir y analizar un programa estructurado útil. Estos temas 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.
Debate
P. J. Plauger, uno de los primeros en adoptar la programación estructurada, describió su reacción al teorema del programa estructurado:
Los convertidos agitaron este interesante pedazo de noticias bajo las narices de los programadores de ensamble-idioma no reconstruidos que seguían tropezando pedazos tortuosos de lógica y diciendo, 'Apuesto que no puede estructurar esto.' Ni la prueba de Böhm y Jacopini ni nuestros repetidos éxitos al escribir código estructurado los trajeron un día antes de lo que estaban listos para convencerse.
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 declaración GOTO y, a partir de 2018, ha seguido usándola en sus programas. En su artículo de 1974, "Programación estructurada con sentencias Goto", 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 menos estricta: 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 sin ramas que se crucen entre sí. Muchos de los expertos en compiladores y teoría de grafos han abogado por permitir solo gráficos de flujo reducibles.
Los teóricos de la programación estructurada ganaron un gran aliado 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 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 para apoyar 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.
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 Considerado Dañino' Considerado Nocivo". Siguieron numerosas objeciones, incluida una respuesta de Dijkstra que criticó duramente tanto a Rubin como a las concesiones que otros escritores hicieron al responderle.
Resultado
A finales del siglo XX, casi todos los informáticos estaban 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.
Desviaciones comunes
Aunque goto ahora ha sido reemplazado en gran medida por construcciones estructuradas de selección (if/then/else) y repetición (while y for), pocos lenguajes están puramente estructurados. La desviación más común, que se encuentra en muchos idiomas, es el uso de una declaración de retorno para salir antes de tiempo 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 incómodos en la programación puramente estructurada.
Salida anticipada
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, esta es una instrucción return
. En el nivel de los bucles, esta es una sentencia break
(terminar el bucle) o una sentencia continue
(terminar la iteración actual, continuar con la siguiente iteración). En la programación estructurada, estos se pueden replicar agregando ramas o pruebas adicionales, pero para los retornos del código anidado, esto puede agregar una complejidad significativa. C es un ejemplo temprano y prominente de estas construcciones. Algunos lenguajes más nuevos también tienen "cortes etiquetados", que permiten romper con algo más que el bucle más interno. Las excepciones también permiten la salida anticipada, pero tienen otras consecuencias y, por lo tanto, se tratan a continuación.
Pueden surgir múltiples salidas 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 ha encontrado "excepcional" circunstancias que le impiden continuar, por lo que necesita un manejo de excepciones.
El problema más común en la salida anticipada es que la limpieza o las declaraciones finales no se ejecutan; por ejemplo, la memoria asignada no se desasigna o los archivos abiertos no se cierran, lo que provoca fugas de memoria o de recursos. Esto debe hacerse en cada sitio de devolución, que es frágil y puede fácilmente generar errores. Por ejemplo, en un desarrollo posterior, un desarrollador podría pasar por alto una declaración de devolución, 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 idiomas sin declaración de retorno, como Pascal estándar y Seed7, no tienen este problema.
La mayoría de los idiomas modernos brindan soporte a nivel de idioma para evitar tales filtraciones; ver discusión detallada en administración de recursos. Por lo general, esto se hace a través de la protección de desenredado, lo que garantiza que se ejecute cierto código cuando la ejecución sale 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 parte del manejo de excepciones. En el caso de múltiples sentencias return
que introduzcan try...finally,
sin excepciones puede parecer extraño. Existen varias técnicas para encapsular la gestión de recursos. Un enfoque alternativo, que se encuentra principalmente en C++, es la adquisición de recursos es inicialización, que utiliza el desenredado normal de la pila (desasignación de variables) al salir de la función para llamar a los destructores en las variables locales para desasignar recursos.
Kent Beck, Martin Fowler y los 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 usa múltiples salidas predicadas por cláusulas de guardia. 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 hagas. Ofrecen una solución de libro de cocina para transformar una función que consta solo de condicionales anidados en una secuencia de instrucciones de retorno (o lanzamiento) protegidas, seguidas de un solo bloque no protegido, que pretende contener el código para el caso común, mientras que las declaraciones protegidas son se supone que debe lidiar con los menos comunes (o con errores). 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.
En su libro de texto de 2004, David Watt escribe que "los flujos de control de entrada única y múltiples salidas suelen ser deseables". Usando la noción de secuenciador del marco de Tennent, Watt describe uniformemente 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 los 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 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 finaliza la ejecución de un comando o procedimiento textualmente adjunto", abarca tanto interrupciones de bucles (incluidos múltiples saltos de nivel) y sentencias de retorno. Watt también señala que si bien los secuenciadores de salto (gotos) se han restringido un poco en lenguajes como C, donde el objetivo debe estar dentro del bloque local o un bloque externo que lo abarque, esa restricción por sí sola no es suficiente para hacer que la intención de gotos en C sea propia. -describiendo y para que todavía puedan producir "código de espagueti". Watt también examina cómo los secuenciadores de excepción difieren de los secuenciadores de escape y salto; esto se explica en la siguiente sección de este artículo.
En contraste con lo anterior, Bertrand Meyer escribió en su libro de texto de 2009 que instrucciones como break
y continue
"son solo los antiguos goto con piel de oveja" y fuertemente desaconsejado su uso.
Manejo de excepciones
Basado en el error de codificación del desastre de Ariane 501, el desarrollador de software Jim Bonang argumenta 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 la salida única debe escribirse de la siguiente manera:
bool MyCheck1() tiro() {} bool éxito = falso; Prueba {} // Haz algo que pueda hacer excepciones. si ()!MyCheck2()) {} tiro SomeInternalException(); } // Otro código similar al anterior. éxito = verdadero; } captura (...) {} // Todas las excepciones atrapadas y registradas. } retorno éxito;}
Peter Ritchie también señala que, en principio, incluso un solo lanzar
justo antes del retorno
en una función constituye una violación del principio de salida única, pero argumenta que Las reglas de Dijkstra se escribieron antes de 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 de carga..
David Watt también analiza el manejo de excepciones en el marco de los secuenciadores (presentado 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 controlador se ubica más naturalmente 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, no se puede establecer una rutina de manejo para esta situación anormal. ubicado en el código del sistema de bajo nivel. Watts señala además que la introducción de pruebas de indicadores de estado en la persona que llama, como lo implicaría la programación estructurada de salida única o incluso los secuenciadores de retorno (salida múltiple), da como resultado una situación en la que "el código de la aplicación tiende a saturarse de pruebas de estado". banderas" y que "el programador podría omitir por olvido o pereza probar un indicador de estado. De hecho, las situaciones anómalas representadas por indicadores de estado se ignoran de forma predeterminada." 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 deliberadamente. Con base 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 excepción dedicado con la semántica discutida anteriormente.
El libro de texto de Louden y Lambert enfatiza que el manejo de excepciones difiere de las construcciones de programación estructurada como los bucles while
porque la transferencia de control "se configura en un punto diferente del programa que aquel donde se produce la transferencia real. En el punto en el que realmente se produce la transferencia, es posible que no haya ninguna indicación sintáctica de que el control se transferirá de hecho." El profesor de informática Arvind Kumar Bansal también señala que en los lenguajes que implementan el manejo de excepciones, incluso las 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 provocar prematuramente una salida anticipada en cualquier parte de la estructura de control; por ejemplo, si init()
arroja una excepción en for (init(); check(); increm())
, entonces el punto de salida habitual después de check() no es alcanzó. Citando múltiples estudios previos realizados por otros (1999-2004) y sus propios resultados, Westley Weimer y George Necula escribieron que un problema importante con las excepciones es que "crean rutas de flujo de control ocultas que son difíciles de razonar para los programadores". 34;.
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 anticipadas desde el interior hacia el exterior 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.
Entrada múltiple
Más raramente, los subprogramas permiten múltiples entradas. Esto es más comúnmente re-ingreso en una corrutina (o generador/semicorutina), donde un subprograma produce el control (y posiblemente un valor), pero luego se puede reanudar donde lo dejó. Hay una serie de usos comunes de dicha programación, en particular para flujos (particularmente de entrada/salida), máquinas de estado y concurrencia. Desde el punto de vista de la ejecución del código, el rendimiento de una corrutina está más cerca de la programación estructurada que el regreso de una subrutina, ya que el subprograma en realidad no ha terminado y continuará cuando se le llame nuevamente; no es una salida anticipada. Sin embargo, las corrutinas significan que múltiples subprogramas tienen un estado de ejecución, en lugar de una sola 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.
Máquinas de estado
Algunos programas, en particular los analizadores y los protocolos de comunicación, tienen varios estados que se suceden entre sí de una manera que no se reduce 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 usa a menudo en el kernel de Linux.
Sin embargo, es posible estructurar estos sistemas haciendo que cada cambio de estado sea un subprograma separado y usando una variable para indicar el estado activo (ver trampolín). Alternativamente, estos pueden implementarse a través de rutinas, que prescinden del trampolín.
Contenido relacionado
Sistema de archivos de alto rendimiento
Barra invertida
Postgresql