Hasta ahora, para escribir nuestros programas hemos definido una serie de acciones dentro del main que se ejecutan en orden secuencial o siguiendo unas estructuras de control. Pero a medida que los problemas van creciendo resulta más fácil primero pensar la solución a un nivel más abstracto, definiendo acciones más generales y después ir concretando cada una de ellas.
Descartes, en el Discurso del Método formuló cómo afrontar la complejidad de un problema:
“dividir cada una de las dificultades que se examinen en tantos fragmentos como sea posible y que se requieran para mejorar la resolución”.
La idea es la misma que la del siguiente proverbio chino:
"las cosas grandes se pueden reducir a cosas pequeñas, y las cosas pequeñas se pueden reducir a la nada".
Así pues, el objetivo será resolver el problema general como una suma de subproblemas más pequeños. Ésta es la base de lo que se llama diseño descendente. En un lenguaje de programación la solución a cada uno de estos subproblemas es lo que se llama subrutina o subprograma, y en C, función. Este planteamiento no sólo nos facilitará la tarea de resolver el problema sino también otros dos aspectos muy importantes: permitir la reutilización del código y facilitar su lectura.
En cuanto a la reutilización del código, piense que si tiene un subprograma escrito que resuelve un subproblema, podrá volver a utilizarlo en otros programas donde también sea necesario resolver el mismo subproblema. Por ejemplo, supongamos que para un programa que debe calcular un número combinatorio escribimos una función que calcula el factorial de un número. Esta misma función la podremos volver a utilizar en otro programa que nos sirva para calcular el desarrollo en serie de ex, donde también es necesario realizar el cálculo de un factorial. Así veamos como la descomposición en funciones favorece la reutilización. También debemos intentar que cuando escribimos un código sea lo más genérico posible para facilitar esta reutilización.
En cuanto a la facilidad de lectura, Dijkstra, uno de los padres de la informática, formuló que:
“por comprender un programa de una sola pieza, un ser humano necesita normalmente un tiempo que aumenta exponencialmente con la longitud del programa”.
Esta afirmación pone en evidencia la dificultad de comprensión de programas muy largos. Hay que tener en cuenta que el ciclo de vida de un programa normalmente no termina cuando el programa se compila y se ejecuta, sino que normalmente existe un posterior mantenimiento, con cambios y adaptaciones del mismo. Así pues, es mucho mejor un programa en el que se han especificado una serie de subprogramas, que uno que se ha hecho sin ninguna estructura interna, línea detrás de línea. La descomposición funcional no es la única técnica que facilita la lectura de un programa. Ya hemos hablado de la utilidad de incluir comentarios dentro del código para hacerlo más comprensible. También ayuda a la lectura del código el uso de tabulaciones para destacar estructuras de control. Por ejemplo, haciendo que las instrucciones que estén dentro de una estructura condicional aparezcan tabuladas, de modo que se vea en primera vista donde comienza y termina la estructura. Esto lo hacen automáticamente muchos de los entornos de desarrollo, al igual que destacar las palabras clave del lenguaje (por ejemplo con un color diferente). Por otro lado, existen lenguajes como el Basic, donde sus estructuras de control son inexistentes y el flujo de ejecución se basa únicamente en instrucciones de saltos de una instrucción a otra, en función de cierta condición. Esto, además de hacer que, por otros causas, los programas sean más ineficientes, hace que el código sea muy difícil de seguir. El uso de los lenguajes estructurados con las estructuras de control que hemos visto (if, while, do while, for) también es importante en este sentido. Aunque algunos de estos lenguajes estructurados disponen de una instrucción goto, de salto de una instrucción a otra, no deben utilizarse.
Lo que haremos en esta unidad es profundizar en cómo se especifican estos subprogramas en qué dividiremos nuestro programa. Es decir cómo escribimos funciones que resuelven uno de los subproblemas del problema general, y cómo hacemos uso después.
Diseño descendente
El método del diseño descendente consiste en empezar trabajando a nivel abstracto para ir dividiendo el problema en sus partes naturales. De esta forma el problema a resolver se descompone en otros más sencillos. A estos últimos se les aplica el mismo procedimiento hasta llegar a problemas lo suficientemente pequeños que podemos resolver directamente. Una forma de entender qué se pretende con este método consiste en olvidar las posibilidades reales del ordenador y suponer que éste es capaz de llevar a cabo cualquier acción. Visto así la resolución del problema será sencilla. Las acciones hipotéticas serán complejas y no tienen porque coincidir con las que es capaz de realizar un ordenador real. En consecuencia la resolución de cualquier problema será sencilla. Una vez encontrado un algoritmo basado en estas acciones hipotéticas habrá que considerar individualmente cada una de estas acciones y plantear las como nuevos problemas encontrando algoritmos que las resuelvan, utilizando si es necesario otros instrucciones genéricas. Este proceso se irá iterando, aumentando más y más el detalle hasta llegar a una solución factible del problema basada en acciones que sí es capaz de realizar un ordenador real. Esto se conoce con el nombre de refinamiento del algoritmo (stepwise refinamiento).
El algoritmo resuelto de esta forma se puede presentar como un árbol donde cada nodo es un módulo, o problema, o solución hipotética. Cada subárbol dependiendo de este nodo se utiliza para la resolución de este subproblema. En particular el nodo del nivel más alto es el problema de partida. El algoritmo será correcto si la solución que se da a cada nivel lo es.
El programa podrá crearse como un todo, teniendo en cuenta el algoritmo completo, pero una solución más razonable es dividirlo en módulos coincidentes con las partes naturales del problema. En este caso se podría construir el programa de abajo arriba creando primero procedimientos que resuelvan los módulos de detalle que, una vez comprobados serán utilizados por otros procedimientos más generales hasta llegar a la creación del programa.