Fundamentos de programación con Módula-2 (Tema 14_1)

Tema 14
Módulos

 

En este Tema se da una introducción a la programación modular, en especial basada en el empleo de tipos abstractos de datos.

Se introduce el concepto de módulo, en general, y su realización en Modula-2, en particular. Se discuten brevemente las características necesarias para compilar módulos por separado, en forma segura, y la manera de conseguirlo en este lenguaje.

Se describe la noción de tipo abstracto de datos, y las técnicas para programarlos usando el mecanismo de módulos en Modula-2. Se menciona la posibilidad de realizar ocultación mediante tipos opacos, con las limitaciones impuestas por el lenguaje.

Finalmente se extiende la metodología de desarrollo de programas con la posibilidad de descomposición en módulos separados, estableciendo las recomendaciones del desarrollo modular basado en abstracciones.

14.1 Concepto de módulo

En programación, un módulo es, en general, un fragmento de programa utilizado en algún momento para la construcción del programa completo. Lo que distingue a un módulo propiamente dicho de un fragmento arbitrario del programa es el hecho de que en algún momento de la construcción haya sido reconocido como tal, y por tanto que se haya desarrollado o refinado en forma relativamente independiente del resto del programa. Podríamos definir:

MÓDULO: Fragmento de programa desarrollado en forma independiente.

El desarrollo independiente debe serlo en el máximo grado posible. Atendiendo a las técnicas de preparación de programas en lenguaje de programación simbólicos, diremos que un módulo debería ser compilado por separado, y no tratarse de un simple fragmento de texto dentro de un único programa fuente.

La razón de exigir compilación por separado para los distintos módulos de un programa obedece a la necesidad de limitar la complejidad de aquello que está siendo elaborado por una persona en un momento dado. Si el módulo se va a compilar por separado, la persona que lo desarrolle podrá concentrarse en él, prescindiendo en parte de cómo se utiliza ese módulo desde el resto del programa. De la misma forma, quien escriba el resto del programa no se preocupará de los detalles de cómo está codificado el módulo, sino sólo de cómo hay que usarlo.

El concepto de módulo, por tanto, está íntimamente ligado a la idea de abstracción. Un módulo debe definir un elemento abstracto (o varios relacionados entre sí) y debe ser usado desde fuera con sólo saber qué hace el módulo, pero sin necesidad de conocer cómo lo hace.

14.1.1 Especificación y realización
Igual que en cualquier otro elemento abstracto, en un módulo podemos distinguir dos puntos de vista, correspondientes a su especificación y a su realización. Tal como se ha indicado con anterioridad, la primera visión nos dice qué hace el módulo, y la segunda nos dice cómo lo hace.

La especificación de un módulo que contenga la definición de una serie de elementos abstractos consistirá, fundamentalmente, en el conjunto de las especificaciones de cada uno de ellos, por separado, más una indicación de los posibles efectos de unos sobre otros cuando se usan en forma combinada.

La realización del módulo consistirá en la realización de cada uno de los elementos abstractos contenidos en dicho módulo.

La especificación del módulo es todo lo necesario para poder usar los elementos definidos en él. Esta especificación constituye la interfaz (en inglés "interface") entre el módulo (incluida su realización) y el programa que lo usa.

La independencia entre la realización de un módulo y el programa que lo usa se incrementa si la realización de un elemento abstracto no esvisible desde donde se usa. Esta castacterística se denomina ocultación, y ha sido ya desarrollada en un Tema anterior para las funciones y procedimientos, explicando el mecanismo de visibilidad por bloques.

Referida a los módulos, la ocultación consiste en que el programa que usa un elemento de un módulo sólo tiene visible la información de la interfaz, pero no la de la realización.

14.1.2 Compilación separada
Los lenguajes de programación que permiten programar usando módulos pueden emplear diversas técnicas para definirlos e invocar los elementos definidos en ellos. Tal como se ha comentado antes, es importante que los módulos puedan compilarse por separado. Esto quiere decir que los lenguajes de programación deben permitir escribir un programa complicado como un conjunto de varios ficheros fuente distintos, cada uno de los cuales pueda compilarse de manera más o menos independiente de los demás.

Por otra parte, para que el uso de los elementos de un módulo sea correcto, habrá que hacerlo de acuerdo con la interfaz establecida. La interfaz debe ser tenida en cuenta al compilar un programa que use elementos de un módulo separado. Por el contrario, la realización del módulo debe permanecer invisible para el programa que lo usa con objeto de mantener la deseable ocultación de los detalles de los elementos abstractos contenidos en él.

En la Figura 14.1 se representa gráficamente la visibilidad deseable entre un módulo y el programa que lo usa.

Figura(14_1)
   Figura 14.1. Visibilidad de un módulo

Resumiendo:

COMPILACIÓN SEPARADA: El programa está formado por varios ficheros fuente, cada uno de los cuales se compila por separado.

COMPILACIÓN SEGURA: Al compilar un fichero fuente el compilador comprueba que el uso de elementos de otros módulos es consistente con la interfaz.

OCULTACIÓN: Al compilar un fichero fuente el compilador no usa información de los detalles de realización de elementos de otros módulos.

Entre las técnicas empleadas por los lenguajes de programación reales en lo que respecta a compilación separada, tenemos situaciones tales como las siguientes:

   a)  El fichero del programa y del módulo se tratan en forma totalmente separada, sin visibilidad de la interfaz (lenguaje FORTRAN y las primeras versiones del lenguaje C).

   b)  La parte necesaria de la interfaz se copia manualmente en el programa que la usa. La compilación de los ficheros del programa y del módulo se hace con total independencia (lenguaje C ANSI, con prototipos, y algunas versiones del lenguaje Pascal, con la directiva EXTERN).

   c)  La interfaz del módulo y su realización se escriben en ficheros separados. El mismo fichero de interfaz se usa tanto al compilar la realización del módulo como al compilar el programa que lo usa (lenguajes Modula-2 y Ada).

En los lenguajes que usan la técnica (a) no hay compilación segura. En los mencionados en (b) la seguridad es mayor, pero aún hay posibilidad de errores si no coincide la interfaz del módulo con la copia usada en el programa. Con los lenguajes que usan la técnica (c) la compilación es completamente segura.

14.1.3 Descomposición modular
La posibilidad de compilar módulos en forma separa permite repartir el trabajo de desarrollo de un programa, a base de realizar su descomposición modular. Los diferentes módulos pueden ser encargados a programadores diferentes, y gracias a ello todos pueden trabajar al mismo tiempo.

De esta forma se pueden desarrollar en un tiempo razonable los grandes programas correspondientes a las aplicaciones de hoy día, que totalizan cientos de miles de sentencias.

La descomposición modular de un programa puede reflejarse en un diagrama de estructura, tal como el de la Figura 14.2. En este diagrama se representa cada módulo en forma de un rectángulo, con el nombre del módulo en su interior, y se usan líneas para indicar las relaciones de uso entre ellos.

Figura(14_2) 
     Figura 14.2. Ejemplo de diagrama de estructura

En este ejemplo el módulo A usa elementos de los módulos B y C, y el módulo B usa elementos de C y D. Los módulos C y D no usan ningún otro módulo. Las líneas que indican relaciones de uso pueden llevar punto de flecha si es necesario indicar expresamente cuál es el sentido de la relación. Normalmente no es necesario, pues, como en este caso, un módulo que usa otro se dibuja encima de él, de manera que las líneas de uso se interpretan siempre de arriba a abajo, estableciendo al mismo tiempo una jerarquía entre módulos.

El objetivo de la ingenería de software es facilitar el desarrollo de la aplicación en forna organizada, y de manera que muchas personas puedan colaborar simultáneamente en un mismo proyecto. Para que la descomposición en módulos sea adecuada, desde ese punto de vista, conviene que los módulos resulten tan independientes unos de otros como sea posible. Esta independencia se analiza según dos criterios, denominados acoplamiento y cohesión.

El acoplamiento entre módulos indica cuántos elementos distintos o característicos de uno o varios módulos han de ser tenidos en cuenta a la vez al usar un módulo desde otro. Este acoplamiento debe reducirse a un mínimo.

La cohesión indica el grado de relación que existe entre los distintos elementos de un mismo módulo, y debe ser lo mayor posible. Esto quiere decir que dos elementos íntimamente relacionados deberían ser definidos en el mismo módulo.

14.2 Módulos en Modula-2

El nombre con que se ha bautizado al lenguaje Modula-2 nos indica ya que debe disponer de mecanismos adecuados para la definición de módulos que pueden ser desarrollados por separado.

Un programa descompuesto en módulos se escribe como un conjunto de ficheros fuente relacionados entre sí, y que pueden compilarse por separado. Cada fichero fuente constituye así una unidad de compilación. A continuación se describen las distintas unidades de compilación que pueden definirse en Modula-2.

14.2.1 Módulo principal
Un programa en Modula-2 se puede descomponer en varios módulos. Uno de estos módulos ha de ser el programa o módulo principal. La ejecución del programa completo equivale, en principio, a la ejecución del módulo principal. Por supuesto, el módulo principal puede invocar durante su ejecución operaciones definidas en otros módulos.

Todos los ejemplos de programas completos desarrollados hasta ahora se componían exclusivamente de un módulo principal. Para ser precisos habrá que decir que en estos ejemplos se usaban módulos "estándar", pero puede considerarse que estos módulos no son parte del programa o aplicación desarrollada.

El módulo principal debe importar explícitamente de los otros módulos todos los elementos que vaya a usar. La manera de escribir un módulo principal ya ha sido expuesta en los Temas anteriores, por lo que no merece la pena dar más explicaciones en este momento.

14.2.2 Módulos de definición
La terminología de Modula-2 es quizá un poco inadecuada en cuanto a los nombres dados a las unidades de compilación, ya que se llama módulo a cada una de ellas. Por una parte tenemos que el programa principal representa de alguna forma la aplicación completa, por lo que puede resultar confuso llamarle "módulo". Por otra parte, cada módulo que forme parte del desarrollo de un programa en Modula-2 ha de escribirse como dos ficheros o unidades de compilación separadas, cada una de las cuales se denomina "módulo" aunque en realidad sean partes de un mismo módulo.

Las dos unidades de compilación de un módulo corresponden, como se había apuntado, a la especificación (o interfaz) y la realización (o "implementación"). La especificación de un módulo se escribe como módulo de definición en Modula-2. La forma general de un módulo de definición es:

                DEFINITION MODULE  Nombre;
                   … listas de importaciones …
                   … definiciones …
                END Nombre.

Las listas de importaciones se escriben igual que para el módulo principal. Las definiciones pueden incluir definiciones de constantes, tipos y variables, y también pueden incluir especificaciones de subprogramas (procedimientos y funciones). Una especificación de subprograma consistirá en la cabecera de la función o procedimiento. Por supuesto, se pueden añadir cuantos comentarios se consideren apropiados para documentar la interfaz del módulo.

Como ejemplo consideramos un módulo que contenga los elementos necesarios para imprimir series de números formando varias columnas a lo ancho del listado. La impresión a varias columnas ya se había desarrollado anteriormente en dos ejemplos del Tema 8. Ahora reuniremos los elementos necesarios para formar con ellos un módulo separado. La interfaz de este módulo puede ser como la que aparece en el siguiente listado:

(*_/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\_
_/-\_
_/-\_   Este módulo contiene los elementos para imprimir series
_/-\_   de números a varias columnas.
_/-\_
_/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\_*)

DEFINITION MODULE Tabulación;
VAR
   numColumnas: INTEGER;               (* número de columnas *)
   ancho: INTEGER                        (* ancho de cada columna*)

PROCEDURE Imprimir( numero: INTEGER );
(* Imprime el número, tabulando *)

PROCEDURE Terminar;
(* Completa la impresión de la última línea *)

END Tabulacion.

Los parámetros de impresión pueden fijarse modificando directamente las variables en la interfaz del módulo. Si no se modifica el valor de estas variables está previsto usar unos parámetros por defecto.

La forma exacta de escribir un módulo de definición se describe mediante la siguientes reglas BNF:

Módulo_definición ::=
          Cabecera_definición
          { Definición_de_elementos }
          END Identificador .

Cabecera_definición ::=
          DEFINITION MODULE Identificador ;
          { Lista_importados ; }
          [ Lista_exportados ; ]

Definición_de_elementos ::=

          Declaración_de_constantes |
          TYPE { Identificador [ = Esquema_de_tipo ] ; } |
          Declaración_de_variables |
          Cabecera_subprograma ;

La lista de exportación sólo se emplea en versiones antiguas de Modula-2, y en ella se enumeran explícitamente los elementos que pueden ser usador por otros módulos, en la forma

                                     EXPORT QUALIFIED   … lista de identificadores …;

En las versiones actuales todos los elementos del "módulo definición" se exportan implícitamente.

14.2.3 Módulos de implementación
Los módulos de implementación de Modula-2 contienen la realización de un módulo. Esta realización tiene exactamente la misma forma que un módulo principal, precedido de la palabra clave IMPLEMENTATION:

                                   IMPLEMENTATION MODULE Nombre;
                                      … lista de importaciones …
                                      … definiciones …

                                   BEGIN
                                      … sentencias de inicialización …
                                   END Nombre.

Las sentencias ejecutables que figuran al final del módulo son opcionales (si no hay sentencias se suprime también la palabra BEGIN). Estas sentencias se ejecutan automáticamente al comienzo de la aplicación, antes de que se ejecute el programa principal,  y antes de que se ejecute la inicialización de cualquier otro módulo que utilice este módulo.

Si examinamos la jerarquía de módulos reflejada en el diagrama de estructura de una aplicación, podemos darnos cuenta de que las partes ejecutables de los módulos (incluyendo el programa principal) se ejecutan estrictamente de abajo a arriba, empezando por inicializar los módulos que no usan a ningún otro, luego los que usan éstos, y terminando por ejecutar el módulo principal.

En el siguiente ejemplo tenemos la realización del módulo para tabular series de números:

(*_/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\_
_/-\_
_/-\_   Este módulo contiene los elementos para imprimir series
_/-\_   de números a varias columnas.
_/-\_
_/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\_*)

IMPLEMENTATION MODULE Tabulacion;
   FROM InOut IMPORT WriteLn, WriteInt;

VAR   columna: INTEGER;              (* columna a imprimir *)

PROCEDURE Iniciar;
(* Prepara la impresión desde la 1ª columna *)
BEGIN
   columna := 1
END Iniciar;

PROCEDURE Imprimir( numero: INTEGER );
(* Imprime el número, tabulando *)
BEGIN
   IF columna > numColumnas THEN
      WriteLn;
       columna := 1
   END;
   WriteInt( numero, ancho );
   INC( columna )
END Imprimir;

PROCEDURE Terminar;
(* Completa la impresión de la última línea *)
BEGIN
   IF columna > 1 THEN WriteLn END;
   Iniciar
END Terminar;

BEGIN
   (*– fijar parámetros por defecto e iniciar impresión –*)
      numColumnas := 4;
      ancho := 10;
      Iniciar

END Tabulacion.

Las sentencias de inicialización asignan valores por defecto a los parámetros y preparan el módulo para imprimir números desde la primera columna.

14.2.4 Uso de módulos
Los elementos definidos en un módulo se pueden usar desde otro, en la forma que ya se ha venido realizando en los módulos estándar. Ahora daremos una visión más completa de los mecanismos de Modula-2 para usar elementos de otros módulos.

Para usar elementos de otros módulos hay que importarlos. La importación se realiza al comienzo del programa (o módulo) que desea usarlos. La forma de importación usada hasta el momento ha sido:

                                FROM nombre_de_módulo IMPORT lista_de_nombre;

Con esta forma de importación los elementos importados se usan directamente por su nombre, tal como si hubieran sido definidos directamente en el programa que los usa.

Existe otra forma de importación, que se escribe simplemente:

                                IMPORT  nombre_de_módulo;

Con esta segunda forma de importación se puede usar cualquier elemento definido en ese módulo, pero no directamente por su nombre, sino designándolo mediante la combinación:

                                nombre_de_módulo . nombre_de_elemento

Esta notación se llama nombre cualificado, y tiene una apariencia similar a la referencia a campos de un registro, ya que también se usa el punto (.) para ligar los dos identificadores usados para la designación de un elemento particular.

A continuación se muestra un sencillo programa para tabular la serie de números del 1 al 20, usando el módulo de tabulación anterior, y empleando los parámetros de tabulación por defecto:

(*_/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\_
_/-\_
_/-\_   Este programa imprime la serie de números del 1 al 29,
_/-\_   a varias columnas.
_/-\_
_/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\_*)

MODULE Serie;

   FROM Tabulacion IMPORT Imprimir, Terminar;

   VAR k: INTEGER;

BEGIN
   FOR k := 1 TO 20 DO
      Imprimir( k )
   END;
   Terminar
END Serie.

El resultado es:

-/@\

En este ejemplo se emplea la misma forma de importación usada habitualmente en los ejemplos anteriores.

Este ejemplo funciona correctamente gracias al orden apropiado en que se ejecutan automáticamente las acciones descritas en la parte ejecutable de cada módulo. La inicialización de módulo de tabulación se ejecuta antes que el módulo principal que lo utiliza. De esta manera al invocar por primera vez la operación de Imprimir las variables de la interfaz del módulo de tabulación contienen los parámetros por defecto y el contador de columnas oculto en su realización está inicializado a 1.

A continuación se muestra un programa similar al anterior, ampliado para mostrar cómo se pueden ajustar los parámetros de tabulación a los valores deseados en cada caso:

(*_/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\_
_/-\_
_/-\_   Este programa imprime la serie de números del 1 al 29,
_/-\_   dos veces, formando diferentes números de columas.
_/-\_
_/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\__/-\_*)

MODULE Serie2;

   IMPORT Tabulacion;

   VAR k: INTEGER;

BEGIN
   (*– imprimir a 3 columnas de 13 caracteres –*)
      Tabulacion.numColumnas := 3;
      Tabulacion.ancho := 13;
      FOR k := 1 TO 29  DO
         Tabulacion.Imprimir( k )
      END;
      Tabulacion.Terminar;
   (*– imprimir a 6 columnas de 5 caracteres –*)
      Tabulacion.numColumnas := 6;
      Tabulacion.ancho := 5;
      FOR k := 1 TO 29 DO
         Tabulacion.Imprimir( k )
      END;
      Tabulacion.Terminar;
END Serie2.

Ahora los resultados son:

-/@\

En este ejemplo se ha usado la forma de importación simple, que exige usar nombres cualificados.

Quizá convenga hacer algunas recomendaciones de estilo con respecto al empleo de nombres cualificados. La referencia simple a elementos importados usando directamente su nombre resulta, en principio, más sencilla. Sin embargo sólo puede considerarse recomendable en aquellos casos en que los elementos nombrados son bien conocidos por todos los que han de consultar en algún momento el texto del programa. Esto ocurre, por ejemplo, con los elementos definidos en los módulos "estándar" asociados al lenguaje.

Por el contrario, cuando una aplicación tiene un número relativamente importante de módulos particulares, el uso de los nombres simples puede acabar siendo confuso, ya que es difícil recordar de memoria en qué módulo está definido cada uno. Incluso se pueden presentar ambigüedades, ya que el nombre de un elemento de un módulo puede coincidir con el otro elemento distinto definido en otro módulo.

En estos últimos casos es preferible usar nombres cualificados, aunque la redacción del programa sea algo más prolija, ya que se gana en precisión, y en cada punto en que se usa un elemento importado queda explícitamente indicado en qué módulo se encuentra definido.

14.3 Tipos abstractos de datos

En programación tradicional, se ha identificado el concepto de tipo de dato con el de la clase de valores que pueden tomar los datos de ese tipo. De hecho esta idea se ha aplicado más a la forma de representación de los valores que a los valores en sí. Por ejemplo, si tenemos que operar con valores correspondientes a los meses del año, lo podremos hacer representándolos mediante números (1 al 12) o bien mediante ristras de caracteres (‘ENE’, ‘FEB’, etc.), entre otras posibilidades. Estas representaciones se asocian con diferentes tipos de datos. En Modula-2 escribiríamos:

                 TYPE TipoMes = INTEGER;

o bien:

                TYPE TipoMes = ARRAY [0..3] OF CHAR;

Un enfoque más moderno de la programación trata de asociar la idea de tipo de datos con la clase de valores, abstractos, que pueden tomar los datos. Esto quiere decir que la representación o codificación particular de los valores no cambia, de hecho, el tipo del dato considerado. Un paso adelante en este sentido ha sido la introducción (en lenguajes como Pascal, Modula-2 o Ada) de los tipos enumerados, en que se definen colecciones de valores, abstractos, asociados a identificadores utilizables dentro del programa.

Los valores de los tipos enumerados no son valores numéricos, ni ristras de caracteres, aunque pueden transformarse en esas otras formas de representación usando apropiadamente los mecanismos del lenguaje. Por ejemplo:

               TYPE TipoMes = (Enero, Febrero, … Diciembre);
               VAR mes: TipoMes;
               . . . .
               WriteInt( ORD(mes)+1, 2 )

En este enfoque más moderno de la programación se identifican los tipos de datos en forma completamente abstracta, llegando a la idea de tipos abstractos de datos. Esto quiere decir que un programa que use ese tipo de datos no debería necesitar ningún cambio por el hecho de modificar la representación o codificación de los valores de ese tipo. Si analizamos con cuidado qué necesita un programa para poder usar datos de un tipo, encontraremos que hace falta:

  • Hacer referencia al tipo en sí, mediante un nombre, para poder definir variables, subprogramas, etc.
  • Hacer referencia a algunos valores particulares, generalmente como constantes con nombre.
  • Invocar operaciones de manipulación de los valores de ese tipo, bien usando operadores en expresiones aritméticas o bien mediante subprogramas.

El conjunto de todos estos elementos constituye el tipo abstracto de datos:

TIPO ABSTRACTO DE DATOS: Agrupación de una colección de valores y una colección de operaciones de manipulación.

Es importante comprender que estas colecciones son cerradas, es decir sólo se deben poder usar los valores abstractos y las operaciones declaradas para ese tipo. Esto no ocurría cuando se asociaba el tipo de valor con su forma de representación. Por ejemplo, si representamos los meses del año mediante números, podremos usar el número 33, aunque no sea ningún mes válido, al igual que podremos multiplicar los números dos meses aunque esto no tenga ningún sentido.

14.4

::= DEFINICION

| alternativa

{} REPETICION

[]OPCION

()AGRUPAR

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s