ESCUELA DE PREPARACIÓN DE OPOSITORES E. P. O. Cl. La Merced, 8 -Bajo A Telf.: 968 24 85 54 30001 MURCIA INF26-SAl28 . Programación modular. Diseño de funciones. Recursividad. Librerías. Esquema. , I INTRODUCCION 1 , 2 PROGRAMA ClON MODULAR 2 2.1 PROGRAMACIÓN CONVENCIONAL 2 2.2 PROGRAMACIÓN MODULAR 3 2.2.1 Concepto demódulo 4 2.2.2 Requisitos de laprogramación modular 5 2.2.3 Ventajase inconvenientes del diseño modular 5 2.2.4 Tipos de módulos 5 2.2.5 Jerarquía de control o estructura delprograma 6 Organización de módulos en niveles y transferencias de control. 6 Programa principal y subprogramas 7 Subprogramas internos y externos 7 Objetos globales y locales 8 2.2.6 Independenciafuncional y calidad del software 8 Cohesión 8 Acoplamiento 8 - 9 3 DISENO DE FUNCIONES 3 .1 PRINCIPIOS DE DISEÑO SEGÚN MEYER 9 3 .2 CRITERIOS DE MODULARIZACIÓN 9 3 .3 REFINAMIENTO Y MODULARIDAD 10 3.4 OCULTACIÓN DE LA INFORMACIÓN 12 3.5 GRADO DE ENTRADA, GRADO DE SALIDA, VISIBILIDAD, Y CONECTIVIDAD 12 3.6 PROCEDIMIENTOS Y FUNCIONES 13 3. 6.1 Abstracción operacional 13 3.6.2 Visión detransferenciade control 13 3. 7 ELEMENTOS BÁSICOS 13 4 RECURSIVIDAD 14 , 5 LIBRERIAS 16 6 CONCLUSIONES 16 1 Introducción. La programación convencional se caracteriza fundamentalmente por la realización de programas monolíticos, es decir un gran programa compuesto de un único módulo. Se presenta el problema de que no puede ser abarcado por un lector. El número de caminos de control, la expansión de las referencias, el número de variables y Escuela de Preparación de Opositores E.P .O. v02 INF26 - SAI28. Página 2 la complejidad global podrían hacer imposible su correcta comprensión. Por otra parte la depuración del programa resulta costosa. Además, otra característica de la programación convencional es la utilización de instrucciones de ruptura de secuencia. Estas instrucciones alteran la secuencia normal de ejecución de instrucciones en un programa que es desde la primera hasta la última, y de una en una, según aparecen escritas. La alteración de esta secuencia hace que continúe en otro lugar definido en la propia instrucción utilizando un número de línea determinado o una etiqueta. Estas instrucciones pueden ser: 1. Instrucciones de salto condicional. Alteran la secuencia normal de ejecución de las instrucciones de un programa si se cumple una condición asociada a la propia instrucción. 2. Instrucciones de salto incondicional. Alteran la secuencia normal de ejecución de las instrucciones de un programa sin la comprobación de una determinada condición. 2 Programación modular. 2.1 Programación convencional La realización de un programa sin seguir un método de programación riguroso, aunque funcione, a la larga no será más que un conjunto más o menos grande de instrucciones. La definición de las diferentes etapas adolecen en general de indefinición y falta de continuidad (desarrollo y mantenimiento). La consecuencia inmediata de lo expuesto anteriormente se podría recoger en la siguiente relación de problemas y defectos que suelen tener los programas escritos sin un determinado método. • Los programas suelen ser excesivamente rígidos, presentando problemas para adaptarlos a las cada día más cambiantes configuraciones. • Los programadores gastan la mayoría de su tiempo corrigiendo sus errores. • Los programadores generalmente rehúsan el uso de programas y módulos ya escritos y en funcionamiento, prefiriendo escribir los suyos. La comunicación entre programadores es muy difícil. • Un proyecto de varios programadores tiene normalmente varios conjuntos diferentes de objetivos. • Cada programador tiene sus propios programas, convirtiéndose esta relación en algo inseparable. • Las modificaciones en las aplicaciones y programas son muy difíciles de hacer; implican mucho tiempo y elevado coste de mantenimiento. Ello conduce a colocar "parches" que complican cada vez más el diseño inicial o bien a que el programa caiga en desuso, y frente al elevado coste de actualización se opte por crear una nueva aplicación que sustituya a la existente. • Deficiencias en la documentación: descripciones incompletas o escasas, ausencia de diagramas, y no actualizada. En esencia, los problemas anteriores y otros no citados imposibilitan la evolución y mantenimiento de los programas. Por consiguiente, es de suma importancia Escuela de Preparación de Opositores E.P .O. v02 INF26 - SAI28. Página 3 prever las futuras modificaciones al objeto de mantener los programas funcionando correctamente y puestos al día. Estas previsiones se pueden resumir en: • Aumento del volumen de datos y estructuras. • Cambios en la organización de la información . • Cambios debidos preferentemente a la modernización de los documentos y sus formatos. • Sustitución, ampliación o reducción en el sistema de proceso de datos. Así pues, se deben prever las posibles modificaciones mediante la creación de programas con la suficiente flexibilidad que sean capaces de adaptarse a los cambios. Se deben crear programas claros, inteligibles y breves con el objetivo de que puedan ser leídos, entendidos y fácilmente modificados. En resumen, se debe establecer una serie de normas que permitan el paso de una programación artesanal a una programación que permita conseguir una estandarización y, en consecuencia, una disminución de los costos informáticos, mayor independencia del programador y seguridad de funcionamiento. A la hora de diseñar un programa, éste debe reunir unas características fundamentales: • Correcto/fiel: producir resultados requeridos. • Legible: debe ser entendido por cualquier programador, que permita fáciles modificaciones. • Modificable: el diseño nunca es definitivo y por ello su estructura debe permitir modificaciones. • Depurable: debe ser fácil la localización y corrección de errores. En resumen, se debe realizar un programa siguiendo técnicas o métodos estandarizados que consigan las características anteriores rápida y eficazmente. Las técnicas de programación más empleadas que permiten seguir una metodología de la programación son la programación modular y la programación estructurada. Estas técnicas suelen ser complementarias, ya que el análisis de un problema puede utilizar criterios de programación modular para dividirlo en partes independientes y utilizar métodos estructurados en la programación de cada módulo. 2.2 Programación modular. El concepto básico de la programación modular es muy simple; consiste en dividir un programa en módulos. En realidad, es un método de diseño que tiende a dividir el problema, de forma lógica, en partes perfectamente diferenciadas que pueden ser analizadas, programadas y puestas a punto independientemente. La división de un. problema en módulos o programas independientes exige otro módulo que controle y relacione a todos los demás; es el denominado módulo base o principal del problema. Realmente la programación modular es un intento para diseñar programas, de forma tal que cualquier función lógica pueda ser intercambiada sin afectar a otras partes del programa. Escuela de Preparación de Opositores E.P .O. v02 INF26 - SAI28. Página 4 Las ventajas de la programación modular se pueden resumir en los siguientes puntos: • Un programa modular es más fácil de escribir y depurar (ejecutar, probar y poner a punto). Se puede profundizar en las pruebas parciales de cada módulo mucho más de lo que se hace en un programa mayor. • Un programa modular es fácil de mantener y modificar . • Un programa modular es fácil de controlar. El desglose de un problema en módulos permite encomendar los módulos más complejos a los programadores más experimentados y los más sencillos a los programadores noveles. • Posibilita el uso repetitivo de las rutinas en el mismo o en diferentes programas. Los inconvenientes se pueden resumir en: • No se dispone de algoritmos formales de modularidad, por lo que a veces los programadores no tienen claras las ideas de los módulos. • La programación modular requiere más memoria y tiempo de ejecución. Se podrían sintetizar los objetivos de la programación modular en los siguientes: • Disminuir la complejidad. • Aumentar la claridad y fiabilidad. • Disminuir el coste. • Aumentar el control del proyecto. • Facilitar la ampliación del programa mediante nuevos módulos. • Facilitar las modificaciones y correcciones al quedar automáticamente localizadas en un módulo. 2.2.1 Concepto de módulo. El concepto de módulo no es único y se pueden dar varias definiciones, aunque tal vez la más acertada sea la siguiente: Un módulo está constituido por una o varias instrucciones físicamente contiguas y lógicamente encadenadas, las cuales se pueden referenciar mediante un nombre y pueden ser llamadas desde diferentes puntos de un programa. Un módulo puede ser: un programa, una función, o una subrutina (o procedimiento). Los módulos deben tener la máxima cohesión y el mínimo acoplamiento. Es decir, deben tener la máxima independencia entre ellos. La salida del módulo debe ser función de la entrada, pero no de ningún estado interno. En esencia, el módulo ha de ser una caja negra que facilite unos valores de entradas y suministre unos valores de salida que sean exclusivamente función de las entradas. En la creación de los módulos deben cumplirse tres aspectos básicos: descripción, rendimiento y diseño. En la descripción se definen las funciones y objetivos del programa. Para obtener el máximo rendimiento se ha de comprobar que el programa realice el proceso aprovechando al máximo todos los recursos de los que dispone. En cuanto al diseño, se debe comprobar la estructura que sigue el módulo, así Escuela de Preparación de Opositores E.P.O. v02 INF26 - SAI28. Página 5 como la estructura de los datos y la forma de comunicaciones entre los diversos y diferentes módulos. 2.2.2 Requisitos de la programación modular. Los requisitos que debe cumplir la programación modular son: a) Establecimiento de un organigrama modular. b) Descripción del módulo principal. e) Descripción de los módulos básicos o secundarios. d) Normas de la programación. El organigrama modular se realiza mediante bloques, en el que cada bloque corresponde a un módulo y muestra gráficamente la comunicación entre el módulo principal y los secundarios. El módulo principal debe ser claro y conciso, reflejando los puntos fundamentales del programa. Los módulos básicos deben resolver partes bien definidas del problema. Sólo pueden tener un punto de entrada y un punto de salida. Si un módulo es complejo de resolver, conviene que se subdivida en submódulos. Ningún módulo puede ser llamado desde distintos puntos del módulo principal. Las normas de programación dependerán del análisis de cada problema y de las normas generales o particulares que haya recibido el programador. 2.2.3 Ventajas e inconvenientes del diseño modular. Las ventajas que presenta la utilización del diseño modular son la inteligibilidad de los programas al dividir los mismos en distintas partes, y la disminución de la dificultad de resolución de los problemas tal y como se deduce del concepto de modularidad descrito anteriormente. Sin embargo, presenta la desventaja de que a medida que se divide el programa en módulos y el número de módulos crece, se produce un incremento de las interfaces entre módulos. 2.2.4 Tipos de módulos Los módulos en los que se puede dividir un programa se pueden clasificar atendiendo a dos criterios: • Según los mecanismos de activación. Existen dos mecanismos de activación. Convencionalmente, un módulo es invocado mediante referencia por ejemplo : una sentencia "de llamada" sin embargo, en las aplicaciones de tiempo real, un módulo puede ser invocado mediante una interrupción; esto es, un suceso exterior produce una discontinuidad en el procesamiento, que da como resultado el paso de control a otro módulo. Los mecanismos de control son importantes porque pueden afectar a la estructura del programa. • Según el camino de control. El camino de control de un módulo describe la forma en que se ejecuta internamente. Los módulos convencionales tienen una única entrada y una única salida y ejecutan secuencialmente una tarea. Algunas veces se necesitan caminos de control más sofisticados. Por ejemplo, un módulo puede ser reentrante. Esto es, un módulo se diseña de forma que de ninguna manera puede modificarse a si mismo o a las Escuela de Preparación de Opositores E.P.O. v02 INF26- SAI28. Página 6 direcciones que referencie localmente. Así, el módulo puede ser usado para más de una tarea concurrentemente. Dentro de una estructura de programa, un módulo puede se clasificado como: • Secuencial, que se referencia y se ejecuta sin interrupción aparente por parte del software de la aplicación. Los módulos secuenciales son los que se encuentran más frecuentemente y están caracterizados como macros en tiempo dè compilación y como subprogramas convencionales (subrutinas, funciones o procedimientos). • Incremental, que puede ser interrumpido, antes de que termine, por el software de la aplicación y, posteriormente, restablecida su ejecución en el punto en que se interrumpió. Los módulos incrementales, denominados frecuentemente corrutinas, mantienen un puntero de entrada que permite volver a ejecutar el módulo desde el punto de interrupción. Dichos módulos son extremadamente útiles en sistemas conducidos por interrupciones. • Paralelo, que se ejecuta a la vez que otro módulo, en entornos de multiprocesadores concurrentes. Los módulos paralelos se encuentran en aplicaciones de cálculo de alta velocidad (por ejemplo procesamiento distribuido) que necesitan dos o más CPUs trabajando en paralelo. 2.2.5 Jerarquía de control o estructura del programa. La jerarquía de control, también denominada estructura del programa, representa la organización (frecuentemente jerárquica) de los componentes del programa (módulos) e implica una jerarquía de control. Para representar la jerarquía de control se utilizan muchas notaciones diferentes. La más común es un diagrama en forma de árbol. Por ejemplo: b d e k Profundidad n l j r Anchura..----------._. , Organización de módulos en niveles y transferencias de control. El diagrama en árbol de la figura anterior muestra las relaciones entre los módulos organizándolos en niveles y conectándolos mediante flechas. La profundidad y la anchura son una indicación del número de niveles de control y de la amplitud global del control, respectivamente. Escuela de Preparación de Opositores E.P.O. v02 fNF26 - SAI28. Página 7 Las relaciones de control entre los módulos se expresan de la siguiente forma: un módulo que controla a otro módulo se dice que es superior a él, e inversamente, un módulo controlado por otro se dice que es subordinado del controlador. Por ejemplo, en la figura, el módulo M es superior a los módulos a, b y c. El módulo h es subordinado del módulo e y en última instancia es subordinado del módulo M. La flecha dibujada entre dos módulos de niveles sucesivos significará que en tiempo de ejecución, el control del programa pasará de un módulo al segundo siguiendo la dirección de la flecha. Se dice entonces que el primer módulo llama al segundo. Cuando un módulo (padre) llama a distintos módulos , es decir, que tenga distintos módulos hijo (en la figura el caso del módulo m), la secuencia de ejecución es de izquierda a derecha. La transferencia de control entre dos módulos, acarrea generalmente la transferencia de datos. Los datos pueden transmitirse en cualquier dirección entre dos módulos. Se pueden transferir dos tipos básicos de información entre módulos: información de control e información de datos. Con la utilización del diseño descendente, la organización de módulos en niveles y la transferencia de control surgen una serie de conceptos que se definen a . . , contmuación. Programa principal y subprogramas. Un programa diseñado mediante la técnica descendente quedará constituido por dos partes claramente diferenciadas: • Programa principal. Describe la solución completa del problema y consta principalmente de llamadas a subprogramas. Estas llamadas son indicaciones al procesador de que debe continuar la ejecución del programa en el subprograma llamado, regresando al punto de partida una vez lo haya concluido. El programa principal puede contener, además, instrucciones primitivas y sentencias de control, que son ejecutables de modo inmediato por el procesador. Un programa principal contendrá pocas líneas, y en él se verán claramente los diferentes pasos del proceso que se ha de seguir para la obtención de los resultados deseados. • Subprogramas. Se les suele denominar declaración de subprogramas. Figuran agrupados en distinto lugar al del programa principal. Su estructura coincide básicamente con la de un programa, con alguna diferencia en el encabezamiento y finalización. En consecuencia, un subprograma puede tener sus propios subprogramas correspondientes a un refinamiento del mismo. La función de un subprograma es resolver de modo independiente una parte del problema, es importante que realice una función concreta en el contexto del problema. Un subprograma es ejecutado por el procesador sólo cuando es llamado por el programa principal o por otro subprograma. Subprogramas internos y externos. Son subprogramas internos los que figuran junto con el programa principal (en el mismo listado). Se denominan de diferentes maneras en los distintos lenguajes de ., programacion: Son subprogramas externos los que figuran fisicamente separados del programa principal, es decir, en distintos archivos fuente. Pueden ser compilados separadamente, Escuela de Preparación de Opositores E.P .O. v02 INF26- SAI28. Página 8 e incluso pueden haber sido codificados en un lenguaje de programación distinto al del programa principal. Generalmente se enlazan con el programa principal en la fase de montaje o enlazado (linkage), cuando ya son módulos objetos, es decir, traducidos a lenguaje máquina. Objetos globales y locales Los diferentes objetos que manipula un programa (constantes variables, tablas, archivos, subprogramas, etc.) se clasifican según su ámbito, es decir, según la porción de programa y/o subprogramas en que son conocidos y, por tanto, pueden ser utilizados. Son objetos globales los declarados en el programa principal, cuyo ámbito se extiende al mismo y a todos sus subprogramas. Son objetos locales a un subprograma los declarados en dicho subprograma, cuyo ámbito está restringido a él mismo y a los subprogramas declarados en él. 2.2.6 Independencia funcional y calidad del software. El concepto de independencia funcional es una derivación directa del de modularidad. La independencia funcional se adquiere desarrollando módulos con "una clara" función y una "aversión" a una excesiva interacción con otros módulos. Dicho de otra forma, se trata de diseñar software de forma que cada módulo se centre en una función especifica de los requisitos y tenga una interfaz sencilla, cuando se ve desde otras partes de la estructura del software. Los módulos independientes son más fáciles de mantener y de probar debido a que se limitan los efectos secundarios producidos por las modificaciones en el diseño/código, se reduce la propagación de errores y se fomenta la reutilización de los módulos. La independencia funcional es la clave de un buen diseño y el diseño es la clave de la calidad del software. La independencia se mide con dos criterios cualitativos: la cohesión y el acoplamiento. Cohesión. La cohesión es una medida de la fortaleza funcional relativa de un módulo. La cohesión permite determinar el correcto particionado de un sistema: examina cómo las actividades dentro de un mismo módulo se relacionan unas con otras. Formalmente se puede definir cohesión como el grado en que los elementos de un módulo realizan una función especifica e indivisible. Por elemento, se identifica una instrucción, o grupo de instrucciones, o una llamada a otro módulo, es decir cualquier trozo de código que acarrea algún trabajo. Se ha de · buscar módulos altamente cohesionados, cuyos elementos estén fuertemente relacionados unos con otros. Por otro lado, los elementos de un módulo no deberían estar fuertemente relacionados con los elementos de otro módulo. Acoplamiento. El acoplamiento es una medida de la interdependencia relativa entre los módulos. Se denomina acoplamiento al grado de interdependencia entre módulos. El Escuela de Preparación de Opositores E.P.O. v02 INF26- SAI28. Página 9 objetivo del diseño será minimizar el acoplamiento, haciendo los módulos tan independientes como sea posible. Un bajo acoplamiento entre módulos indica una buena partición del sistema. La conectividad sencilla entre módulos da como resultado un software que es más fácil de comprender y menos propenso al "efecto onda" producido cuando los errores aparecen en una posición y se propagan a lo largo del sistema. 3 Diseño de funciones. 3.1 Principios dediseño según Meyer. Meyer establece cinco principios que debe cumplir un buen método de diseño: • Descomponibilidad: El método de diseño debe ser útil para establecer una descomposición funcional adecuada. • Componibilidad: Una vez esté descompuesto un problema y esté solucionado, se puede utilizar dicha división funcional en la resolución de otros problemas. Esto determina el grado de reusabilidad de los módulos. • Comprensibilidad : Si la función no es comprensible para otro uso fuera del contexto en que está inmerso no va a poder ser reusada. • Continuidad: Pequeños cambios de requisitos de la aplicación debe representar pequeños cambios en la codificación o diseño de la aplicación. • Protección : Se debe procurar que ante la presencia de un fallo, éste se propague poco, es decir, evitar la existencia de los efectos laterales. 3.2 Criterios de modularización. La división de un programa en módulos debe cumplir los siguientes criterios: a) Cada módulo debe corresponder a una función lógica perfectamente diferenciada. b) El tamaño de cada módulo es variable. Deben ser pequeños para que sean claros y de poca complejidad. Las normas varían según las situaciones. Como norma general práctica se puede considerar el tamaño máximo de un módulo como una página de listado de impresora. e) Evitar variables externas. d) Procurar no utilizar demasiados niveles de modularización para evitar complejidad de la red. e) Estructura de caja negra para cada módulo (la salida debe ser función exclusiva de la entrada). Las técnicas de programación modular no aportan nuevos conceptos desde el punto de vista de proceso de la información, sino más bien un método o filosofía para la descomposición idónea de un problema en problemas o módulos más sencillos. Como ya se ha comentado en apartados anteriores, se pueden utilizar criterios de programación modular y posteriormente utilizar métodos de programación estructurada dentro de cada módulo independiente. Una vez terminado el diseño de los programas de cada módulo, realizar el montaje del programa completo mediante un diseño ascendente. Escuela de Preparación de Opositores E.P.O. v02 INF26 - SAI28. Página 10 3.3 Refinamiento y modularidad. El refinamiento sucesivo es una primera estrategia de diseño descendente propuesta por Niklaus Wirth. La arquitectura de un programa se desarrolla en niveles sucesivos de refinamiento de los detalles procedimentales. Se desarrolla una jerarquía descomponiendo una declaración macroscópica de una función de una forma sucesiva, hasta que se llega a las sentencias del lenguaje de programación. El refinamiento es realmente un proceso de elaboración. Se comienza con una declaración de la función (o una descripción de la información). Es decir, la declaración describe la función o la información conceptualmente, pero no proporciona información sobre el funcionamiento interno de la función o sobre la estructura interna de la información. El refinamiento hace que el diseñador amplíe la declaración original, dando cada vez más detalles conforme se produzcan los sucesivos refinamientos (elaboraciones). El concepto de modularidad se refiere al hecho de que el software se divida en componentes con nombres y ubicaciones determinados, que se denominan "módulos" y que se integran para satisfacer los requisitos del problema Se ha dicho que "la modularidad es el atributo individual del software que permite a un programa ser intelectualmente manejable" porque facilita la comprensión del programa. Para ilustrar este punto, consideremos la siguiente disquisición, basada en observaciones sobre la resolución humana de problemas. Sea C(x) una función que define la complejidad de un problema x y E(x) una función que define el esfuerzo (en tiempo) requerido para resolver un problema x. Para dos problemas, pl y p2, si C(pl)>C(p2) se deduce que E(pl)>E(p2). Para un caso general, este resultado es intuitivamente obvio. Se tarda más tiempo en resolver un problema difícil. Se ha encontrado otra propiedad interesante, a partir de la experimentación sobre la resolución humana de problemas, es la siguiente: C(pl+p2)>C(pl)+C(p2), que indica que la complejidad de un problema compuesto porpl yp2 es mayor que la complejidad total cuando se considera cada problema por separado. Se puede deducir que E(pl+p2)>E(pl)+E(p2). Indica que es más fácil resolver un problema complejo cuando se divide en trozos más manejables. Se podría concluir de la desigualdad anterior que, si partiéramos el software indefinidamente, el esfuerzo requerido para desarrollarlo seria insignificantemente pequeño. Sin embargo conforme crece el número de módulos, el esfuerzo (coste) asociado a las interfaces entre los módulos también crece. Por lo tanto, debe evitarse tanto una excesiva modularización como una pobre. Pero, cómo de modular debe hacerse el software, el tamaño de un módulo dependerá de su función y de su aplicación. Las fases de la resolución de un problema con programación modular son las siguientes: • Estudio de las especificaciones del problema. • Confección del ordinograma o tabla de decisión de cada módulo. • Codificación de cada módulo en el lenguaje adecuado. • Pruebas parciales de cada uno de los módulos componentes. • Prueba final de los módulos enlazados. Escuela de Preparación de Opositores E.P .O. v02 INF26- SAI28. Página 11 El diseño de una aplicación con programación modular consiste en la realización de una red de módulos. Existirá un módulo raíz, que se denomina principal o director. Cada módulo sólo puede tener una entrada y una salida que lo enlazan con el módulo principal, incluso habiendo estructuras repetitivas y alternativas dentro de un módulo. La programación modular se basa en el diseño descendente (top-down) que permite comprobar el funcionamiento de cada módulo mediante módulos ya comprobados. Módulo Raíz Módulo 1 Módulo 2 Módulo 3 Módulo 4 Módulo 5 Módulo 6 Módulo 7 Módulo 8 Módulo 9 Módulo 10 Módulo 11 Módulo 12 Módulo 14 ....__ -..; Módulo 13 El montaje de la red se hace en modo ascendente (bottom-up), por lo que un programador puede estar escribiendo el módulo 11 mientras otro hace lo propio con el módulo 12. Una vez terminados ambos se puede comprobar su funcionamiento con un ficticio 7 que llama a ambos. De igual forma se pueden escribir los módulos 1 O y 14 al tiempo que los 11 y 12, etc. Los datos que forman parte de un programa modular se dividen en dos grandes grupos: variables internas o locales y variables externas o globales. Las variables internas son utilizadas por un solo módulo y las variables externas por más de un módulo. Los módulos se comunican entre sí por las variables externas que técnicamente son las más importantes en el desarrollo del programa. Los módulos tienen que utilizar las variables, bien como referencia o como modificador de su valor. El diseño descendente o diseño de arriba abajo (top-down) utiliza los conceptos de refinamiento y modularidad descritos anteriormente. Consiste en una serie de descomposiciones sucesivas del problema inicial, que describen el refinamiento progresivo del conjunto de instrucciones que van a formar parte del programa. La utilización de esta técnica de diseño tiene los siguientes objetivos básicos: • Simplificación del problema y de los subprogramas resultantes de cada descomposición. Escuela de Preparación de Opositores E.P.O. v02 INF26 - SAI28. Página 12 • Las diferentes partes del problema pueden ser programadas de modo independiente e incluso por diferentes personas. • El programa final queda estructurado en forma de bloques o módulos, lo que hace más sencilla su lectura y mantenimiento. 3.4 Ocultación de la información. El principio de ocultación de información sugiere que los módulos se han de "caracterizar por decisiones de diseño que los oculten unos a otros". En otras palabras, los módulos deben especificarse y diseñarse de forma que la información (procedimientos y datos) contenida dentro de un módulo sea inaccesible a otros módulos que no necesiten tal información. La ocultación implica que para conseguir una modularidad efectiva hay que definir un conjunto de módulos independientes, que se comuniquen con los otros sólo mediante la información que sea necesaria para realizar la función de software. La ocultación establece y refuerza las restricciones de acceso a los detalles procedimentales internos de un módulo y a cualquier estructura de datos localmente utilizada en el módulo. El uso de la ocultación de información como criterio de diseño para los sistemas modulares, revela sus mayores beneficios cuando se hace necesario realizar modificaciones, durante la prueba y, más adelante, el mantenimiento del software. Debido a que la mayoría de los datos y de los procedimientos estarán ocultos a otras partes del software, será menos probable que los errores introducidos inadvertidamente durante la modificación se propaguen a otros lugares del software. 3.5 Grado de entrada, grado de salida, visibilidad, y conectividad. El grado de entrada (fàn-in) de un módulo es el número de módulos que le llaman directamente. Un fan-in elevado es el resultado una factorización (separación de una función contenida como código en un módulo, como un nuevo módulo) inteligente y de la eliminación de módulos restrictivos. A la hora de programar, el hecho de tener una función llamada por diferentes módulos evita la necesidad de codificar prácticamente la misma función en distintos lugares. No debe preocupar el hecho de que las llamadas se produzcan desde módulos de diferentes niveles. Si el módulo es verdaderamente útil puede utilizarse por cualquier otro módulo del sistema. Sin embargo, hay que tener en cuenta dos reglas que restringen el uso del fan-in: • Los módulos con fan-in deben tener una buena cohesión. • Cada interfaz hacia un módulo sencillo debe tener el mismo número y tipo de parámetros. El grado de salida (fan-out) de un módulo es el número de módulos inmediatamente subordinados de ese módulo. Se ha de intentar limitar el fan-out de un módulo. Un módulo con demasiados subordinados puede remediarse mediante la factorización. La visibilidad (scope) indica el conjunto de componentes del programa que pueden ser invocados o utilizados sus datos por un componente dado, incluso cuando se haga indirectamente. Por ejemplo, un módulo en un sistema orientado a los objetos puede tener acceso a muchos objetos de los que herede, pero puede que sólo utilice unos pocos de esos objetos. Todos los objetos son visibles para el módulo. Escuela de Preparación de Opositores E.P.O. v02 INF26 - SAI28. Página 13 La conectividad indica el conjunto de componentes a los que directamente se invoca o se utilizan sus datos en un determinado módulo. Por ejemplo, un módulo que directamente puede provocar la ejecución de otro módulo, está conectado a ese último. 3.6 Procedimientos yfunciones. Un procedimiento está formado por un conjunto de sentencias a las que se asocia un identificador, y que realizan una acción que se reconoce por los cambios que se producen en un conjunto de variables o por realizar alguna operación de entrada/salida. Para realizar su acción el procedimiento puede recibir una serie de valores a través de un conjunto de variables o parámetros que acompañan su definición. Las variables que son modificadas pueden ser variables de las que actúan como parámetros o no serlo. Una función está constituida por un conjunto de sentencias a las que se asocia un identificador, y cuyo efecto se manifiesta porque producen un valor que es asignado al nombre de la función. Para generar este valor pueden recibir un conjunto de valores a partir de unas variables o parámetros que se especifican en su declaración. Una vez definida una función o un procedimiento podemos usarlo en cualquier parte del programa donde necesitemos realizar la acción que desarrolla, con tan solo invocarlo mediante su nombre. Las dos posibles visiones que podemos ofrecer de ellos son la abstracción operacional y la transferencia de control. 3.6.1 Abstracción operacional. Esta es la que corresponde a la descripción que acabamos de ofrecer de los procedimientos. El programa le pasa unos valores de entrada y el procedimiento realiza una acción que consiste en modificar el estado de ciertas variables, o en el caso de las funciones que consiste en devolver un valor. Por tanto, para usarlos sólo es necesario conocer qué valores es necesario suministrarle y conocer cómo manifiesta su efecto, es decir saber si es una función o un procedimiento y en este caso conocer las variables que modifica. Pero no nos interesa el cómo hace la operación. 3.6.2 Visión de transferencia de control. Cuando la ejecución de un programa llega a una sentencia de invocaci6n de un procedimiento, el control se transfiere hacia la primera sentencia de éste. Cuando el procedimiento llega a su final, el control se transfiere a la sentencia que sigue a aquella donde se produjo la invocación. 3. 7 Elementos básicos. Los elementos básicos que presenta un módulo son: • Identificador. Nombre que sirve para invocarlo desde cualquier parte del programa. • Lista de parámetros. Los parámetros son una lista de cero o varias variables que permiten la comunicaci6n entre un procedimiento y el programa que lo usa (que también puede ser otro procedimiento). Cuando son declarados se especifica esta lista y cuando son invocados es necesario que una lista de argumentos acompañe al nombre. Se realiza una correspondencia uno a uno Escuela de Preparación de Opositores E.P .O. v02 INF26 - SAI28. Página 14 entre parámetro y argumento. Los argumentos son expresiones del mismo tipo de dato que el parámetro que aparece en la misma posición en la lista de parámetros. Estos permiten construir procedimientos generales, ya que es posible definir una operación y en el momento de la invocación establecer sobre qué objetos de datos se realiza. Los parámetros pueden ser de dos tipos: Parámetros formales: variables locales de un subprograma utilizadas para la recepción y el envío de los datos. Son siempre fijos. Parámetros actuales: variables y datos enviados, en cada llamada de subprograma, por el programa o subprograma llamante. pueden ser cambiados para cada llamada. • Cuerpo. El cuerpo es el conjunto de sentencias que corresponden a la especificación de la operación. • Entorno. Es el conjunto de variables externas al procedimiento que son accesibles y pueden ser modificadas o simplemente usadas. 4 Recursividad. La recursividad es una técnica que permite que un subprograma se llame a si mismo para resolver una versión reducida del problema original. Frente a una determinada gama de problemas se puede optar por una solución iterativa (no recursiva) o una solución recursiva. Existen situaciones en las que el uso de la recursividad permite soluciones (programas) mucho más simples. No obstante, no conviene abusar de esta herramienta, pues podría dar lugar a resultados impredecibles y de difícil comprensión. El uso de esta técnica es apropiado especialmente cuando el problema a resolver o la estructura de datos a procesar tienen una clara definición recursiva. Se dice que un objeto es recursivo, si en parte está formado por sí mismo o se define en función de sí mismo. La recursividad es una técnica especialmente eficaz en las definiciones matemáticas. Unos ejemplos muy conocidos son los números naturales, las estructuras de árbol y ciertas funciones: 1. Números naturales: • O es un número natural. • El sucesor de un número natural es otro número natural. 2. Estructuras de árbol: • O es un árbol (llamado árbol vacío). • Si tl y t2 son árboles, entonces las estructuras formadas por un nodo con dos árboles descendientes también son un árbol. 3. La función factorial n! (para enteros no negativos): • O!= 1 • n > O: n! = n * (n - 1) ! Escuela de Preparación de Opositores E.P .O. v02 INF26 - SAI28. Página 15 El poder de la recursividad radica, sin duda, en la posibilidad de definir un conjunto infinito de objetos mediante una proposición finita. De la misma manera, un número infinito de cálculos puede describirse con un programa recursivo finito, pese a que no contenga repeticiones explícitas. Sin embargo, los algoritmos recursivos son idóneos principalmente cuando el problema por resolver, la función por calcular o la estructura de datos por procesar ya están definidos en términos recursivos. En general, un programa recursivo P puede expresarse como una composición P de un conjunto de proposiciones S (que se contiene P) y P. La herramienta necesaria y suficiente para expresar los programas recursivamente es el procedimiento o subrutina, pues permite dar a una proposición un nombre con el cual puede ser llamada. Si un procedimiento P contiene una referencia explícita a sí mismo, se dice que es directamente recursivo; si P contiene una referencia a otro procedimiento Q, que incluye una referencia (directa o indirecta) a P, entonces se dice que P es indirectamente recursivo. El uso de la recursión puede, pues, no ser evidente en el texto del programa. Se acostumbra asociar un conjunto de objetos locales a un procedimiento, esto es, un conjunto de variables, constantes, tipos y procedimientos que se definen localmente a este procedimiento y que carecen de existencia o significado fuera de este procedimiento. Cada vez que ese procedimiento se activa de modo recursivo, se crea un nuevo conjunto de variables locales acotadas. Aunque tienen el mismo nombre que sus elementos correspondientes en el conjunto local al caso anterior del procedimiento, sus valores son específicos y se evita cualquier conflicto en la imposición de nombre por medio de las reglas de la cobertura (ámbito) de los identificadores: éstos siempre se refieren al conjunto de variables de creación más reciente. La misma regla se aplica a los parámetros de procedimiento, los cuales por definición están vinculados al procedimiento. A semejanza de las proposiciones repetitivas, los procedimientos recursivos introducen la posibilidad de computaciones que no terminan y, con ello, también la necesidad de tener presente el problema de la terminación. Un requisito fundamental es, sin duda, que las llamadas recursivas de P estén sujetas a una condición B, que en un momento dado resulta falsa. En las repeticiones, la técnica básica de demostrar la terminación consiste en 1. Definir una función f(x) (x será el conjunto de variables) tal que f(x)O para la condición asegura entonces la terminación. En las aplicaciones prácticas es obligatorio demostrar que la profundidad final de la recursión no sólo es finita, sino que en realidad es muy pequeña. Ello se debe a que, luego de cada activación recursiva de un procedimiento P, cierta cantidad de memoria se necesita para alojar sus variables. Además de estas variables locales, el estado actual de la computación debe registrarse a fin de que sea recuperable cuando finalice la nueva activación de Py se reanude la anterior. Escuela de Preparación de Opositores E.P.O. v02 INF26 - SAI28. Página 16 5 Librerías. El proceso de generación de programas ha evolucionado constantemente con el fin de obtener una mayor eficiencia de los programas y un mejor aprovechamiento de los recursos del sistema. Inicialmente se observó que muchos módulos eran utilizados una y otra vez por diferentes programas, por consiguiente se llegó a la conclusión que sería bueno agrupar todas aquellas funciones de propósito general en librerías de módulos. De esa forma se puede desarrollar nuevo software reutilizando aquellos módulos ya escritos. Una librería no es más que un archivo donde están agrupados varios módulos o funciones, de tal forma que el enlazador (linker) entiende su formato. Para un acceso más rápido del enlazador se añade un índice al principio del fichero que ayuda a identificar los módulos e identificadores que contiene sin tener que recorrer todo el fichero. Este tipo de librerías se denominan librerías estáticas. En los sistemas multitarea, cuando se ejecutan dos códigos diferentes que utilizan la misma librería, una forma de ahorrar memoria es cargar en memoria una única vez el código de la librería, ya que el código es idéntico para los dos programas, y utilizarlo ambos. Esto da lugar a la aparición de las librerías dinámicas. En este caso el programa no se enlaza por completo, sino que las referencias a identificadores de librerías compartidas se posponen para el proceso de carga del mismo. El enlazador reconoce que se está ante una librería compartida y no incluye el código de ésta en el programa. Cuando el programa se ejecuta, se reconoce que es un programa con librerías compartidas y es justo en ese momento cuando proceden a cargarse, o en el caso de que ya estén en memoria se hace un enlace a la dirección donde están almacenadas. Las librerías dinámicas, al contrario que las estáticas, no son archivos sino conjuntos de objetos reubicables, marcados con un código especial que los identifica como librerías compartidas. El enlazador, no añade al código del programa los módulos, sino que selecciona como resueltos los identificadores aportados por la librería y continúa sin añadir el código de la misma al programa. Para que una librería dinámica sea apropiada debe ser utilizada la mayor parte del tiempo por algún programa; esto evita el problema de cargar los módulos de la librería, ya que permanece cargada en memoria tras la muerte del proceso que la usa, al haber otros procesos utilizándola. La librería dinámica se carga en memoria completa, no sólo los módulos utilizados, así que es conveniente que se utilice en su totalidad. No son buenas librerías dinámicas aquéllas donde sólo se usa una función y el resto de las funciones no se utilizan la mayor parte del tiempo. Por el contrario, en una librería estática, no importa nada incluir funciones cuyo uso sea infrecuente, ya que siempre que dichas funciones ocupen un módulo propio, no serán enlazadas en aquellos programas que no las usen. 6 Conclusiones. Conforme se extiende el tamaño de los algoritmos, se hace más difícil su revisión, actualización y/o corrección. Una política común para solventar este problema consiste en la modularización. Esto significa que el algoritmo se fragmenta en partes llamadas módulos. En realidad, es un método de diseño que tiende a dividir el problema, de forma lógica, en partes perfectamente diferenciadas que pueden ser analizadas, programadas y puestas a punto independiente. Escuela de Preparación de Opositores E.P.O. v02 INF26 - SAI28. Página 17 Un módulo es aquél que está constituido por una o varias instrucciones físicamente contiguas y lógicamente encadenadas, las cuales se pueden referenciar mediante un nombre y pueden ser llamadas desde diferentes puntos de un programa. Los módulos deben tener la máxima cohesión y el mínimo acoplamiento. Es decir, deben tener la máxima independencia entre ellos. La salida del módulo debe ser función de la entrada, pero no de ningún estado interno. Realmente la programación modular es un intento para diseñar programas, de forma tal que cualquier función lógica pueda ser intercambiada sin afectar a otras partes del programa. Un módulo puede ser: un programa, una función, o una subrutina (o procedimiento). Para el diseño de los módulos se utiliza el diseño descendente (top­ down) que permite comprobar el funcionamiento de cada módulo mediante módulos ya comprobados. El montaje de un programa se hace en modo ascendente (bottom-up) a partir de los módulos existentes. La recursividad es una técnica que permite que un subprograma se llame a si mismo para resolver una versión reducida del problema original. El uso de esta técnica es apropiado especialmente cuando el problema a resolver o la estructura de datos a procesar tienen una clara definición recursiva. Las funciones de propósito general se agrupan en librerías de módulos. De esa forma se puede desarrollar nuevo software reutilizando aquellos módulos ya escritos. Una librería no es más que un archivo donde están agrupados varios módulos o funciones.