Lenguajes & Paradigmas

Desarrollo & Construcción de Software

Programación Estructurada

Paradigmas & Modelos de Abstracción

Javier Vélez Nov 2023 19 mins

Introducción

No tengo memoria suficiente para atestiguar que efectivamente fue así, pero supongo que fue una cuestión de hastío. La comunidad de científicos - lo que décadas después se convertiría en desarrolladores e ingenieros de sofware - llevaban ya demasiados años escribiendo programas de ordenador en base a acrónimos en ensamlador o, lo que es peor, directamente en código binario. Y así, debieron concluir que había llegado el momento de cambiar el nivel de abstracción en los procesos de especificación de algoritmos. Habían nacido, de esta manera, los lenguajes de programación de tercera generación.

Aunque caracterizados por hacer uso de un conjunto de sentencias e instrucciones mucho más próximas al lenguaje humano los esfuerzos de desarrollo todavía consistían en una especificación muy próxima al entendimiento del computador. Sin embargo, comparativamente, esta evolución supuso todo un cambio generacional en aquéllos días, ya que a partir de aquél momento, la redacción algorítmica podría reformularse en términos de sentencias de control de alto nivel y expresiones soportadas por un sistema de tipos fuerte.

Objetivos

Tendrían que pasar algunos años para que la comunidad científica advirtiera que, en realidad, la programación estructurada era tan sólo un modelo más de diseño y desarrollo de programas comparable al de otros paradigmas. Seguro que por aquella época - estamos hablando de los incipientes años 50 - los científicos estaban mucho más ocupados jugando con su nueva criatura que intentando hacer cuerpo teórico de las cosas y creando metadiscursos en relación a modelos de desarrollo y marcos conceptuales de los paradigmas.

A tenor de la reflexión que hacíamos en la introducción que habría este artículo, podemos advertir que la programación estructurada de aquella época vino a dar respuesta a ciertos puntos de dolor en los que los procesos de desarrollo habían incurrido. Y esto, confirió, a esta nueva aproximación de desarrollo, un espacio de objetivos determinados, lo que años después permitiría identificarlo como un paradigma de programación característico en relación con otras aproximaciones de construcción de software. Sin el ánimo de ser exhaustivos ni rigurosos, podemos citar los siguientes como objetivos centrales de la programación estructurada.

  • Legibilidad. La legibilidad del código fuente puede considerarse, claramente, como uno de los objetivos centrales del nuevo paradigma. Recordemos que, en aquellos años, el desarrollo de algoritmos consistía en un uso masivo de prolijas secuencias de construcciones sintácticas basadas en acrónimos de ensamblador que limitaban el ámbito de entendimiento no mucho más allá del de cada instrucción. La llegada de los lenguajes de tercera generación con sentencias expresadas a partir de construcciones cercanas al lenguaje humano fue un soplo de aire fresco en relación a la legibilidad del código.

  • Estructuración. Si la legibilidad fue el gran salto generacional para la experiencia del desarrollador, sin duda alguna, el segundo gran avance fue en relación a la estructuración del código. A diferencia de lo que venía ocurriendo en la programación basada en ensamblador, ahora la algoritmia podía expresarse en términos de sentencias de control de flujo de programa para crear construcciones iterativas y condicionales. Además, los programas podían descomponerse en términos de procedimientos independientes encapsulados para formar estructuras compositivas basadas en invocación paramétrica.

  • Reusabilidad. Este último punto - el de la descomposición de la complejidad en términos de procedimientos encapsulados - permitió empezar a establecer las bases de la reutilización que se convertiría en uno de los objetivos más generales y transversales de la ingeniería del software. Hasta la fecha, a través de la programación en ensamblador, la reutilización de fragmentos de código sólo había sido posible mediante el uso de saltos de control en el flujo natural del programa controlados a demasiado bajo nivel como para que no fueran una fuente de propensión de errores.

  • Mantenibilidad. Con todo lo anterior, la estructuración obtenida por los lenguajes de tercera generación, propios de este paradigma, había conseguido un alto grado de mantenimiento del código, al menos en comparación retrospectiva con respecto a lo que venía siendo la experiencia de desarrollo hasta la fecha sobre la base de los paradigmas de bajo nivel. Con la llegada de los depuradores de código, que permitían hacer una introspección del estado de la máquina durante la ejecución, el mantenimiento correctivo se había convertido en una actividad ágil y productiva. Los esfuerzos de mantenimiento evolutivo también se convirtiera en una realidad ya que alterar por extensión o adaptación el código fuente construido en una iteración anterior era algo que empezaba a ser asumible.

  • Escalabilidad. Finalmente, en adición a todo lo anterior, la llegada de los montadores aplicada a las fases ulteriores de la compilación, permitiría distribuir el código fuente en distintos ficheros organizados en estructuras de proyecto con complejidad creciente. A esta respecto quizás es conveniente establecer dos fases de madurez dentro de la historia de este paradigma. En una primera etapa, la programación estructurada se caracterizaba por construir software más monolítico centrado en el uso de sentencias de control de flujo. En una segunda etapa, el desarrollo se convirtió en actividades de diseño más modular donde el código fuente se fragmentaria en abstracciones primero procedimentales y luego centradas en datos convenientemente distribuidas en ficheros diferentes.

Si bien nuestro análisis retrospectivo ha dibujado a la programación estructurada más como una colección de objetivos conseguidos a posteriori que como un conjunto de metas por alcanzar en el esfuerzo de ideación de un nuevo paradigma, lo cierto es que en suma, estos objetivos, describen un cambio generacional que supuso un revulsivo en la forma de concebir los procesos de desarrollo y construcción de software.

Principios

El cambio de concepción en el desarrollo que supuso la programación estructurada no solamente tuvo un impacto tecnológico, sino también un alcance cultural y metodológico que impactaría a la experiencia de desarrollo y en los procesos de construcción de software. Con un código mucho más manejable y entendible pudo abrirse la puerta a un espacio de reflexión acerca de cómo debería ser abordado el desarrollo de productos estructurados.

La reflexión más relevante en este punto se materializó en forma de principios de diseño, especialmente vinculados al diseño modular de productos de software. Aunque estas líneas de actuación se vinculan en el tiempo a la etapa tardía de la programación estructurada, lo cierto es que sentarían las bases para formalizar los procesos de construcción de software ante una demanda creciente de digitalización cada vez más compleja y elaborada.

  • Descomposición Modular. El principio de descomposición modular establece que todo programa debe idearse de acuerdo a un proceso descomposición descendente. Cada programa comienza por un módulo global, que se erige como el único punto de arranque del sistema y éste se va descomponiendo en otros módulos con capacidad más reducida. La actividad de diseño consiste en iterar este proceso hasta alcanzar módulos considerados lo suficientemente simples como para no requerir una nueva descomposición. Asi pensado, el diseño resultante es siempre un árbol de descomposición en la que unos módulos dependen siempre de un módulo padre que los gobierna y controla en ejecución.

  • Composición Modular. Si el principio de descomposición modular expresaba el énfasis fuertemente jerárquico del paradigma estructurado, el principio de composición reflejaba una nueva orientación en el diseño modular basado en el uso de abstracciones de datos y procedimientos que podrían ser universalmente explotados en el marco de un problema. De esta manera, el diseño de todo módulo así concebido, debería ser diseñado promoviendo su capacidad compositiva para ser usado con el resto de módulos del programa en cualquier contexto arquitectónico presente o futuro.

  • Abstracción Modular. La composición modular se encuentra fuertemente vinculada al principio de abstracción modular que establece que todo módulo debe diseñarse con el nivel adecuado de abstracción. En efecto, cuanto más abstracto es el diseño de un módulo, más oportunidades tendrá éste de ser utilizado en diferentes contextos arquitectónicos y mayor será su probabilidad de que aparezca en un mayor número de composiciones. Dentro del paradigma estructurado, la abstracción se lleva a cabo por medio de la parametrización de operaciones del módulo y su configuración global.

  • Ocultación Modular. Un buen diseño modular también debe mantener el aislamiento de cada módulo con respecto al resto de módulos de la arquitectura. Con respecto a la etapa de diseño esto significa que el espacio de decisiones tomado en los procesos de implementación y desarrollo de la lógica interna de un módulo no deben afectar a las formas en las que dicho módulo es consumido por el resto de módulos de la vecindad arquitectónica que lo explota. Esta idea, también conocida como el principio de protección de información, pasaría años después en materializarse dentro de otros lenguajes y paradigmas de programación a través de los conocidos mecanismos de encapsulación.

  • Continuidad Modular. El aislamiento que mantiene un módulo con el resto de módulos de la arquitectura también conduce al principio de continuidad modular. Este principio establece que un cambio en la especificación de un problema solo debe provocar cambios en el módulo responsable y no debe tener repercusiones relevantes en el resto de los módulos de la vecindad arquitectónica allá donde dicho módulo está siendo utilizado. El uso del término continuidad en este contexto hace referencia a la idea de que un delta de cambio en el espacio del problema debe repercutir siempre solamente en un delta de cambio en el espacio de la solución.

  • Protección Modular. El aislamiento modular tiene una tercera y última declinación relevante en relación al diseño de programas. Esta es recogida en el principio de protección modular, que establece que en ningún caso el comportamiento particular obtenido por un modulo ante contextos de fallo debe tener percusiones de propagación relevantes más allá de los límites establecidos por dicho módulo. Dicho de otra forma, cada módulo funciona como estructura de auto contención ante la propagación de errores y anomalías.

Los principios de diseño que hemos descrito en esta sección tienen una importancia relevante no solamente desde un punto de vista metodológico sino también desde una perspectiva histórica. A través de ellos, no sólo se establecieron las bases de cómo debería abordarse el diseño y la construcción de productos de software basados en el paradigma estructurado sino que también se sentaría un precedente relevante en relación a cómo debe ser descrito un paradigma. Si bien es necesario establecer matices y excepciones al respecto, sí podemos afirmar que, en términos generales, la mayoría de estos principios tienen una aplicabilidad transversal en cualquier actividad de diseño hecha sobre la base de este u otros paradigmas venideros.

Mecanismos

Los lenguajes de soporte al paradigma de programación estructurada dieron en llamarse, en sus orígenes, lenguajes de tercera generación porque tras los dos esfuerzos anteriores - binario y ensamblador - éstos constituían la tercera generación de lenguajes de soporte a la programación. Resulta hilarante pero a partir de aquí la comunidad de desarrollo dejó de contar.

Desde un punto de vista cronológico, estos lenguajes pueden organizarse en dos fases o etapas temporales. La primera etapa corresponde a la segunda mitad de los 50, momento en el que aparecen lenguajes como, Fortan, Algol, Cobol y hasta Basic si se quiere. La segunda generación desarrollada a partir de los 70, incorpora a escena 2 lenguajes que crean escuelas de soporte complementarias. De un lado el stream de C caracterizado por estructuras de programa planas, con alcances de visibilidad reducido y sistemas e tipos débiles y de otra la escuela de los Pascal, con una estructura fuertemente jerárquica, alcance no local y sistemas de tipos fuertes. A esta segunda corriente pertenecen lenguajes posteriores como Ada o Modula entre otros.

Por poner un poco de orden en toda esta sopa de propuestas, merece la pena describir el conjunto de mecanismos de soporte que todo lenguaje de programación estructurada debe proporcionar tentativamente. Si bien no todos estos mecanismos se desarrollaron a un mismo tiempo ni tampoco aparecen con un soporte universal en todos los lenguajes de programación estructurada, sí podemos considerar que la siguiente numeración es considerablemente completa.

  • Encapsulación. La unidad de desarrollo dentro de la programación estructurada es el procedimiento o función. Un procedimiento es una encapsulación nominal de un algoritmo con alta cohesión interna y bajo acoplamiento externo. Los procedimientos son los ciudadanos de primera categoría dentro del paradigma y ello significa que en un programa estructurado todo debe concebirse como una colección de procedimientos operando de manera coordinada para mostrar un comportamiento específico en tiempo de ejecución. Esta coordinación se establece en base a la invocación de unos procedimientos por otros de acuerdo al diseño jerárquico resultante de la descomposición modular descendente que explicamos con anterioridad.

  • Abstracción. De acuerdo a lo anterior, el diseño de cada procedimiento supone una actividad de abstracción, en tanto a que ésta puede ser expresada en términos paramétricos de invocación que son resueltos en tiempo de ejecución. Cuanto mayor es el grado de abstracción para métrica de un procedimiento, mayor es su potencial de reutilización para ser invocado en diferentes contextos arquitectónicos de uso.

  • Estructura. La estructuración de código proporcionada por los lenguajes de soporte es, sin lugar a dudas, otro de los mecanismos más relevantes del paradigma. De acuerdo al enfoque de diseño basado en descomposición modular descendente, todo programa debe concebirse como una estructura jerárquica de bloques algorítmicos enlazados por medio de la invocación. En el plano del código, esto significa que cada procedimiento debe poder encapsular el conjunto de procedimientos hijos en los que delega y debe codificar la lógica de orquestación que coordina el uso de todos ellos en tiempo de ejecución.

  • Control. Para dotar de riqueza expresiva la definición de la lógica de orquestación que caracteriza cada procedimiento, los lenguajes de soporte al paradigma de programación estructurada, deben proporcionar mecanismos de control de flujo. En este sentido, se distinguen tres familias fundamentales que se materializan en diferentes sabores sintácticos según cada lenguaje particular. Nos estamos refiriendo al control de flujo secuencial, al control de flujo iterativo y al control de flujo condicional.

  • Modularidad. Aunque la modularidad se desarrolló muchos años después de la aparición del paradigma lo cierto es que se trata de un mecanismo esencial en la programación estructurada especialmente en aquellos casos en los que la complejidad estructural y volumétrica de las soluciones es considerable. A través de este mecanismo los desarrolladores son capaces de distribuir la lógica de un programa estructurado en diferentes ficheros y de acuerdo a diferentes criterios de organización del proyecto lo que ayuda en el gobierno y mantenimiento de los activos arquitectónicos.

Todos los mecanismos propios de la programación estructurada antedescritos operan sobre un marco de trabajo basado en la idea de ejecución imperativa según el cual un algoritmo es responsable de leer y escribir variables ambientales que caracterizan el estado del programa en ejecución a lo largo del tiempo. De esta manera, se entiende que la responsabilidad de todo algoritmo consiste en mover el estado de la máquina desde un inicio hasta el final a través de procesos de transformación convergente. Una descripción detallada de los distintos modelos de ejecución puede encontrarse en otra serie subsiguiente.

Arquitecturas

La característica esencial de todas las arquitecturas del paradigma estructurado es que disponen de un único punto de entrada que recibe el nombre de programa principal. Esto es una consecuencia natural del diseño de descomposición modular descendente según el cual todo programa debe concebirse como una descomposición jerárquica de módulos que dependen unos de otros en tiempo de ejecución de acuerdo a una relación de paterno filialidad.

Esta restricción da forma general a todas las arquitecturas hasta tal punto que si contemplamos un programa estructurado desde su unidad de construcción, observaremos que cada procedimiento es en realidad un algoritmo que centraliza la invocación coordinada de los procedimientos hijos que agrega mediante el uso de cierta estrategia de control de flujo.

Esta idea, propia de los primeros años del paradigma, esta asociada a la corriente más académica, que como explicábamos más arriba tiene sus representantes más notables en lenguajes como Pascal, Ada o Modula. Con el transcurso de los años - y la llegada de aires nuevos como son especialmente las contribuciones de Wirth hablando de tipos abstractos de datos - este enfoque de jerarquización fuerte se relajó y lenguajes de nueva generación como C entre otros plantearon modelos más planos y débilmente tipados. La lectura en negativo criticaba que estas nuevas propuestas conducían a un mayor grado de anarquía y polución de código, visto al menos, desde la perspectiva clásica. La lectura en positivo, más modernista si se quiere, defendía que las estructuras planas permitían una estructuración modular de código más abierta y reutilizable, más horizontal y menos vertical. Sin duda alguna, se estaban gestando las ideas que años mas tarde, formalizaría la orientación a objetos.

Aunque no es fácil entrar a categorizar acerca de diferentes modelos arquitectónicos de referencia aplicables especialmente al marco del paradigma estructurado, sí podemos a continuación describir tres arquitecturas preponderantes que tuvieron, a lo largo de los años, una aplicación recurrente en diferentes contextos.

  • Arquitecturas de Orquestación. El tipo más convencional de arquitecturas de software que podemos encontrar dentro del paradigma de la programación estructurada da lugar a soluciones basadas en la orquestación. En ellas, se desarrolla un cuerpo algorítmico con mayor o menor grado de modularidad caracterizado por tener un módulo global como único punto de entrada y tal vez cierta variabilidad paramétrica. La responsabilidad de este módulo es la de orquestar toda la ejecución de forma autónoma de cara a ofrecer un resultado de salida final.

  • Arquitecturas de Procesos. En las arquitecturas de procesos, el modelo de solución se establece como una colección de procedimientos secuencialmente alternados con una colección de almacenes o bases de información. En este tipo de arquitecturas toda ejecución comienza por el primer procedimiento de la cadena que recoge datos del primer almacén y deposita resultados de salida en el almacén subsiguiente detrás de él en la cadena. La ejecución continúa a lo largo de la cadena hasta que el resultado final de la operación pueda recogerse en el último almacén.

  • Arquitecturas de Eventos. Las arquitecturas de eventos consisten en una colección de procedimientos que ofrecen manejadores de actuación en respuesta a determinados eventos o señales ambientales. Este tipo de arquitecturas tiene, como elemento central, un loop de eventos responsable de capturar cada una de lod eventos emitidos y disparar, como respuesta reactiva, los procedimientos asociados cuando resulte pertinente. Este tipo de arquitecturas fue un modelo muy popular para hacer frente a la construcción de sistemas interactivos propios de aquellos años.

Patrones

En sentido estricto, la cultura de documentar conocimiento de diseño en base a catálogos y lenguajes de patrones no llegaría a estar establecida hasta bien entrada la década de los años 90, cuando la orientación a objetos estaba en pleno apogeo. Sin embargo, de manera retrospectiva, podemos citar algunos modelos que, en el plano del desarrollo de soluciones locales, venían siendo de aplicación recurrente.

En concreto, es posible reconocer tres familias de problemas fundamentales que, dentro del espacio de la programación estructurada, aparecen de manera pertinaz y para los cuales se han identificado, a largo de la historia, soluciones de aplicabilidad probada. De alguna forma, estas familias de soluciones habrían sido identificadas como patrones de diseño si en aquella época este término hubiera estado acuñado.

  • Patrones de Recorrido. Los patrones de recorrido se centran en dar una respuesta canónica a problemas relacionados con el recorrido y el procesamiento de estructuras de datos. De acuerdo a los dogmas paradigmáticos, la programación estructurada se concibe como la definición de procedimientos capaces de realizar un procesamiento de los datos paramétricos de entrada para arrojar unos resultados a la salida. Cuando este procesamiento se basa en la consumición de todos los datos de una estructura de información caemos en este tipo de patrones. El recorrido secuencial de un array o el recorrido en profundidad o en anchura de un árbol son ejemplos de problemas de esta índole. Asimismo, las actividades de comprobación lógica y filtrado sobre estructuras de datos también se consideran patrones de esta categoría.

  • Patrones de Búsqueda. Los patrones de búsqueda llevan a cabo procesos de recorrido sobre estructuras de datos que tienen por objeto localizar a elementos determinados dentro de las mismas. La actividad de búsqueda puede ser total o parcial y los criterios de localización pueden ser de diferente índole. Por ejemplo, puede interesarnos localizar la posición de un determinado elemento, comprobar la existencia de un elemento dentro de la estructura o recuperar la colección de todos los elementos que cumplen cierta condición.

  • Patrones de Ordenación. Los patrones de ordenación tienen por objeto alterar el contenido de una estructura de datos para establecer una nueva disposición de sus elementos en base a un criterio de ordenación. Este tipo de patrones, puede considerarse una extensión de los patrones de recorrido centrados en realizar actividades de transformación sobre las estructuras de datos de entrada para generar una respuesta ordenada a la salida. De acuerdo a esta idea, las operaciones de ordenación pueden clasificarse en aquellas que respetan la estructura de entrada y generan otra a la salida o aquellas que se basan en algoritmos que destruyen la información original para disponerla de la forma solicitada.

Técnicas

Las técnicas de desarrollo empleadas por los programadores dentro del paradigma estructurado dan respuesta a cómo se utilizan los diferentes mecanismos del lenguaje, y en particular las herramientas de control de flujo para establecer arquetipos sintácticos que, de alguna manera, hacen frente a problemas frecuentes.

Mucho de este conocimiento, en realidad, cae dentro de modismos y estilos de desarrollo y tiene una expresión fuertemente centrada en el uso de las capacidades del lenguaje y sus sintaxis particulares. Precisamente por este motivo, no queremos caer en la tentación de describir tal espacio de soluciones. Por hacernos una idea del tipo de cosas que estamos hablando podemos pensar en una estructura de iteración de código con un punto de ruptura definido por medio de una expresión de centinela condicional. Entendemos que entrar en el detalle exhaustivo de este tan bajo nivel de descripción que hay fuera de los objetivos de los artículos de esta serie.

Conclusiones

La programación estructurada nació como una respuesta para hacer frente a los problemas que experimentaban hasta la fecha los desarrolladores al tener que realizar actividades de codificación en lenguaje binario o ensamblador. Los esfuerzos de mantenimiento correctivo y evolutivo eran casi una ensoñación y era necesario idear un nuevo modelo de sintaxis más centrado en la expresión humano que ayudará a liberarse del código para pensar más en el espacio del problema y su solución.

Para los ingenieros de lenguajes, responsables de construir compiladores, este propósito fue un gran reto que tardo en afinarse algo más de 20 años. Pero, a mediados de los 50, los lenguajes así llamados de tercera generación, vieron la luz. Este hecho tuvo una gran aceptación y supuso un gran revulsivo dentro de la comunidad ingeniería del software.

Ahora los expertos podían centrar sus actividades en definir metodologías y procesos de alto nivel, principios y buenas prácticas y técnicas y patrones que ayudar a la comunidad de desarrolladores a realizar las actividades de programación de forma organizada, sistemática, homogénea y estándar.

A lo largo de este artículo, hemos prestado especial atención, precisamente, a estos aspectos más formales de los procesos de desarrollo de software. Aquellos que darían forma a un nuevo paradigma conocido como el nombre de programación estructurada. Como en todas las ideas, esta aproximación tenía sus limitaciones y lagunas y no tardaron en salirle detractores e impulsores de nuevas ideas disruptivas. Pero describir esta transición es ya objeto de un artículo distinto.