Fundamentos de programación con Módula-2 (9)

 

Tema 4
Metodología de Desarrollo de Programas (I)
 
 
Aquí se tratan explícitamente los primeros conceptos metodológicos relacionados con la programación en general y con la programación imperativa en particular. Se presenta el desarrollo por refinamientos sucesivos, aunque limitado al empleo de la estructura secuencial, por el momento.
 
Como elementos  complementarios, se hace explícita la necesidad de usar un buen estilo de programación, que se concreta en sugerencias sobre el uso de notación apropiada para la representación de los elementos del programa y la presentación de su estructura en forma clara.
 
 
4.1 La programación como resolución de problemas
 
La labor de programación puede considerarse como un caso particular de la resolución de problemas. Resolver un problema consiste esencialmente en encontrar una estrategia a seguir para conseguir la solución.
 
               Problema                                                 Solución
 
  altura         ¿área?                                            área = base x altura
                  base
 
Una estrategia se expresará como una colección de reglas o recomendaciones que, si se siguen, conducirán a la solución. Por ejemplo, para resolver el problema de obtener el área de un rectángulo del que se conocen las longitudes de sus lados (base y altura), formularemos la estrategia:
 
                  multiplicar la base por la altura
 
Un programa en el modelo de programación imperativa se expresa como una serie de instrucciones u órdenes que gobiernan el funcionamiento de una máquina. La máquina va ejecutando dichas instrucciones en el orden preciso que se indique. La estrategia del ejemplo anterior se expresaría en Modula-2 como:
 
                 area := base*altura
 
Todo programa puede considerarse, de alguna forma, como la solución de un problema determinado, consistente en obtener una cierta información de salida a partir de unos determinado datos de entrada. La tarea de desarrollar dicho programa equivale, por tanto, a la de expresar la estrategia de resolución del problema en los términos del lenguaje de programación utilizado.
 
 
4.2 Descomposición de un problema en subproblemas
 
Algunos problemas, como el ejemplo anterior referente al área del rectángulo, pueden resolverse/programarse en forma inmediata porque la estrategia se puede expresar como una acción que se da por sabida, o como sentencia en el lenguaje de programación elegido. Los problemas resolubles de esta manera serán, en general, problemas simples o triviales.
 
Cualquier problema de cierta complejidad necesitará una labor de desarrollo para expresar la solución. El método más general de resolución de problemas no triviales consiste en descomponer el problema original en subproblemas más sencillos, continuando el proceso hasta llegar a subproblemas que puedan ser resueltos en forma directa.
 
Lo que se busca aquí es una analogía con la labor de desarrollo de programas. Puesto que el proceso de ejecución de un programa imperativo consiste en la realización de las sucesivas acciones indicadas por las órdenes que constituyen el programa, analizaremos la resolución de problemas en que la estrategia de solución consiste en realizar acciones sucesivas.
 
Según esta idea, para desarrollar la estrategia de resolución, habrá que ir identificando subproblemas que se resolverán ejecutando acciones cada vez más simples. Consideremos el siguiente problema:
 
              Problema: Obtener una caja de madera barnizada
 
Para expresar la estrategia de solución en forma imperativa comenzamos por formular la solución como una acción global que consigue el objetivo propuesto. En nuestro caso será:
 
              0) Construir una caja de madera barnizada
 
En este primer nivel de formulación nos encontramos con una acción a realizar que es demasiado complicada para ejecutarla en forma inmediata. Será necesario descomponer el problema original en subproblemas más sencillos, que puedan ser resueltos mediante acciones más simples. Un primer paso de descomposición sería:
 
              1) Obtener las piezas de madera
              2) Montar la caja
              3) Barnizarla
 
El proceso de descomposición en subproblemas debe continuar hasta que los subproblemas se puedan resolver mediante acciones consideradas directamente ejecutables por el agente que ha de proporcionar la solución. La expresión final de la solución del problema debe tener en cuenta, por tanto, qué acciones particulares se consideran realizables en forma directa. Tenemos así una analogía con la tarea de programación, que exige redactar el programa con los elementos particulares del lenguaje de programación elegido.
 
En nuestro ejemplo habrá que decidir si el subproblema 1) ha de considerarse resoluble mediante una acción simple o compuesta. Si podemos adquirir las piezas directamente en una tienda de "bricolaje", podremos considerar que el subproblema es resoluble con una acción simple que no necesita descomponerse. Si en la tienda sólo podemos comprar un tablero de madera sin cortar, tendremos que descomponer el problema de obtener las piezas en subproblemas más sencillos, tales como:
 
             1.1) Obtener un tablero de madera
             1.2) Dibujar sobre él la silueta de las piezas
             1.3) Recortar el tablero siguiendo la silueta
 
De igual manera habría que proceder con los subproblemas planteados hasta este momento: 1.1), 1.2), 1.3), 2) y 3), decidiendo si son resolubles en forma inmediata o bien descomponiéndolos sucesivamente hasta llegar a acciones simples.
 
 
4.3 Desarrollo de programas por refinamientos sucesivos
 
La aplicación de las ideas anteriores a la construcción de programas conduce a la técnica de desarrollo mediante refinamientos sucesivos. Esta técnica es parte de las recomendaciones de una metodología general de desarrollo de programas denominada programación estructurada, que se estudiará con más detalle en el Tema siguiente. La técnica de refinamiento consiste en expresar inicialmente el programa a desarrollar como una acción global, que si es necesario se irá descomponiendo en acciones más sencillas hasta llegar a acciones simples, que pueden ser expresadas directamente como sentencias del lenguaje de programación.
 
Cada paso de refinamiento consiste en descomponer cada acción compleja en otras más simples. Esta descomposición exige:
 
  • identificar las acciones componentes
  • identificar la manera de combinar las acciones componentes para conseguir el efecto global
La forma en que varias acciones se combinan en una acción compuesta constituye el esquema o estructura de la acción compuesta. La programación estructurada recomienda el uso de esquemas particularmente sencillos, que se indicarán más adelante. Por el momento presentamos un primer esquema que denominaremos esquema secuencial, y que consiste en realizar una acción compuesta a base de realizar una tras otra, en secuencia, dos o más acciones componentes. Este esquema secuencial es el que se ha utilizado en el ejemplo del problema de construir la caja de madera.
 
4.3.1 Desarrollo de un esquema secuencial
La metodología de refinamientos incluye el ir desarrollando a la vez las sentencias del programa que realizan las acciones de la parte ejecutable, y la definición de las variables necesarias para almacenar la información manipulada por dichas acciones. Para desarrollar una acción compuesta según un esquema secuencial se necesitará:
 
        a) Identificar las acciones componentes de la secuencia. Identificar las variables necesarias para disponer de la información adecuada al comienzo de cada acción, y almacenar el resultado.
        b) Identificar el orden en que deben ejecutarse dichas acciones.
 
Para ilustrar esta técnica consideraremos un caso simple, tal como el de obtener la suma de dos números enteros. Los dos números se introducirán como datos y el programa suministrará como resultado su suma. Aunque resulte un ejemplo trivial, sirve perfectamente para ilustrar la metodología de desarrollo.
 
Procediendo paso a paso, describiremos de manera informal cada elemento del desarrollo, seguido de su codificación en Modula-2.
 
a) Acciones componentes:
            Cálculos: obtener la suma
                      suma := dato1 + dato2
            Operaciones de entrada: leer datos
                      WriteString("Dar dos números: ");
                       ReadInt( dato1 );
                       ReadInt( dato2 );
                       WriteLn
             Operaciones de salida: imprimir resultado
                       WriteString( "La suma es" );
                       WriteInt( suma, 10 );
                       WriteLn
     Variables necesarias: datos y resultado
                        dato1, dato2, suma: INTEGER;
 
b) Orden de ejecución:
            1) Leer los datos
            2) Calcular la suma
            3) Imprimir el resultado
 
Si nos limitamos a describir la manera en que la acción global del programa se va descomponiendo en acciones cada vez más sencillas, podemos usar la siguiente notación de refinamiento:
 
              Acción compuesta —>
                    Acción 1
                    Acción 2
                    … etc. …
 
En esta notación se utiliza una flecha (—>) para indicar que una acción complicada se descompone o refina en otras más sencillas. Aplicando esta notación al ejemplo anterior, hasta llegar a sentencias de Modula-2, tendremos:
 
                 Obtener la suma de dos números —>
                       Leer los datos
                       Calcular la suma
                       Imprimir el resultado
 
                  Leer los datos —>
                      WriteString( "Dar dos números: ");
                      ReadInt( dato1 );
                      ReadInt( dato2 );
                      WriteLn
 
                  Calcular la suma —>
                      suma := dato1 + dato2
                  
                   Imprimir el resultado —>
                      WriteInt( suma, 10 );
                      WriteLn
 
Uniendo todos los fragmentos finales de código en el orden adecuado, y añadiendo las declaraciones de las variables necesarias, tendremos el programa completo.
 
MODULE Sumar2;
  FROM InOut IMPORT WriteString, WriteLn, ReadInt, WriteInt;
  VAR
    dato1, dato2, suma : INTEGER;
BEGIN
  WriteString( "Dar dos números: " );
  ReadInt( dato1 );
  ReadInt( dato2 );
  WriteLn;
  suma := dato1 + dato2;
  WriteString( "La suma es" );
  WriteInt( suma, 10 );
  WriteLn
END Sumar2.
 
Un ejemplo de la ejecución de este programa sería:
 
Dar dos números: 37 143
La suma es       180
 
4.3.2 Ejemplo: Imprimir la silueta de una silla
Aplicaremos la técnica de refinamientos a un programa sencillo en Modula-2 que imprima en forma esquemática la silueta de una silla usando caracteres normales de escritura, por ejemplo "!" y "=", en la forma:
 
                               !
                               !
                               ======
                               !         !
                               !         !
 
La parte ejecutable del programa se planteará inicialmente como una acción única, que trataremos de describir con una frase sencilla; por ejemplo:

                            Imprimir la silueta de una silla

 
Puesto que no parece fácil programar esta acción con una única sentencia de Modula-2, la descompondremos en acciones más simples. De forma intuitiva podemos:
 
   a) Asociar las acciones componentes a la impresión de diferentes partes de la silla: asiento, patas y respaldo.
   b) Seguir el orden de ejecución impuesto por el hecho de que la impresora ha de ir imprimiendo las líneas de arriba a abajo.
 
El primer paso de refinamiento será:
 
         Imprimir la silueta de una silla  —>
             Imprimir la silueta del respaldo
             Imprimir la silueta del asiento
             Imprimir la silueta de las patas
 
Ahora hay que determinar si estas acciones intermedias son ya expresables directamente como sentencias en Modula-2 o necesitan nuevas descomposiciones. No hay reglas fijas sobre cuándo una acción puede considerarse simple y pasar a escribirla en el lenguaje de programación. En este ejemplo consideraremos simple la escritura de una línea de texto (aunque haya que hacerlo con dos sentencias de Modula-2, para incluir el salto de línea).
 
De acuerdo con ello podremos considerar como acción simple la impresión del asiento, y refinar:
 
            Imprimir la silueta del asiento  —>
                WriteString( "======" ); WriteLn
 
La impresión del respaldo se refinará en la forma:
 
            Imprimir la silueta del respaldo  —>
                Imprimir parte superior del respaldo
                Imprimir parte inferior del respaldo
 
y cada una de estas partes conducirá a las mismas sentencias en Modula-2:
 
            Imprimir parte superior o inferior del respaldo  —>
                WriteString( "!" ); WriteLn
 
De forma similar se refinaría la impresión de las patas de la silla. Omitimos este paso para no hacer el ejemplo demasiado prolijo. Finalmente todos estos refinamientos han de ser combinados en un programa único. Reuniendo todas las sentencias en una única secuencia tendremos:
 
                 WriteString( "!" ); WriteLn;
                 WriteString( "!" ); WriteLn;
                 WriteString( "======" ); WriteLn;
                 WriteString( "!         !" ); WriteLn;
                 WriteString( "!         !" ); WriteLn
 
 
4.4 Aspectos de estilo
 
Una buena metodología de desarrollo de programas debe atender no sólo a cómo se van refinando las sucesivas acciones, sino a cómo se expresan las acciones finales en el lenguaje de programación. El estilo de redacción del programa en su forma final es algo fundamental para conseguir que sea claro y fácilmente comprensible por parte de quienes hayan de leerlo.
 
En los apartados que siguen se darán diversas recomendaciones sobre la manera de presentar adecuadamente un programa para facilitar su comprensión.
 
4.4.1 Encolumnado
Un programa aparece como un texto. Dicho texto puede ser visto como un documento técnico, organizado de una manera muy precisa. El estilo de presentación de dicho texto o documento debe destacar claramente su organización en partes.
 
Un recurso de estilo de presentación es el sangrado o encolumnado. Ampliando el margen izquierdo para las partes internas del programa se puede conseguir que el texto de un elemento compuesto ocupe una zona aproximadamente rectangular, y que el texto que representa cada una de sus componentes ocupe también una zona rectangular dentro de ella. Gráficamente podemos ilustrar esta idea con el esquema de la figura siguiente. En la figura se ha marcado el espacio de cada parte como un rectángulo, y se indica con línea doble cuál es el margen izquierdo en cada momento.
 
click aqui
 
Si aplicamos este recurso de estilo al esquema global de un programa en Modula-2, llegaremos a escribir, ya completo, el ejemplo anterior de la siguiente manera:
 
MODULE Silla;
   FROM InOut IMPORT WriteString, WriteLn;
BEGIN
   WriteString( "!" ); WriteLn;
   WriteString( "!" ); WriteLn;
   WriteString( "======" ); WriteLn;
   WriteString( "!         !" ); WriteLn;
   WriteString( "!         !" ); WriteLn
END Silla.
 
Las palabras clave MODULE, BEGIN y END delimitan el programa completo, y marcan la separación en sus dos partes, la primera de importación y declaraciones, y la segunda de sentencias ejecutables.
 
4.4.2 Comentarios. Documentación del refinamiento
Otro recurso utilizable para mejorar la claridad de un programa es el empleo de comentarios. Ya se ha indicado cómo el lenguaje Modula-2 permite intercalar comentarios en el texto de un programa escribiéndolos entre los símbolos (* y *).
 
Aunque el lenguaje permite emplear comentarios con toda libertad, es aconsejable seguir ciertas pautas para facilitar la lectura del programa. Estas pautas corresponden a diferentes clases de comentarios, cada una con un propósito diferente. Entre ellas podemos mencionar:
 
  • Cabeceras de programa
  • Cabeceras de sección
  • Comentarios-orden
  • Comentarios al margen
La cabecera de programa tiene como finalidad documentar el programa como un todo. Puede incluir datos de identificación, finalidad, descripción general, etc. Suele presentarse como una "caja" al comienzo del texto del programa, ocupando todo el ancho del listado. A continuación se presenta una posible cabecera para el programa de ejemplo de imprimir la silueta de una silla.
 
(*********************************************************
*
* Programa: SILLA
*
* Autor: J.A. Cerrada
*
* Descripción:
*   Este programa imprime en forma esquemática la
*   silueta de una silla usando caracteres
*   normales de la impresora.
*
**********************************************************)
 
En este ejemplo la "caja" se ha delimitado con asteriscos, y se ha dejado abierta en el  lado derecho para facilitar la edición o modificación del texto de la misma.
 
Las cabeceras de sección sirven para documentar partes importantes de un programa relativamente largo. Al igual que la cabecera del programa, se presentan en forma de "caja" al comienzo de la sección correspondientes, ocupando todo el ancho del listado. Un ejemplo trivial sería:
 
(*========================================================
                      PARTE EJECUTABLE DEL PROGRAMA
==========================================================*)
En este ejemplo la "caja" se ha delimitado con signos "=" y se ha dejado abierta a ambos lados.
 
Los comentarios-orden son un elemento metodológico fundamental, y sirven para documentar los refinamientos empleados en el desarrollo del programa. Si analizamos la redacción final del programa de ejemplo de la silla nos encontraremos con que las acciones intermedias que se han ido identificando durante el proceso de desarrollo por refinamientos sucesivos no aparecen en ninguna parte del texto del programa. Al hacerlo así se ha perdido una información de gran importancia para realizar posteriormente modificaciones en el programa.
 
Para incluir la información de los pasos de refinamiento en el texto del programa se puede introducir un comentario con la descripción de cada acción intermedia. Estos comentarios tienen, en cierta medida, la categoría de una orden del programa imperativo; en efecto, si el lenguaje de programación lo permitiese, el deseo del programador sería escribir una sentencia que ejecutara dicha acción, pero como no es posible, la acción se descompone en otras más sencillas que dan lugar finalmente a sentencias del programa. El comentario-orden se debe encolumnar tal como se haría con una sentencia del lenguaje que ejecutara la acción deseada. El comentario-orden delimita una acción compuesta, y las acciones componentes se escribirán dejando un mayor margen a la izquierda, tal como se indicó en la sección anterior. Como ejemplo, se repite aquí la parte ejecutable del programa de imprimir la silueta de la silla, documentando las acciones principales con comentarios-orden.
 
            (*– Imprimir el respaldo –*)
               WriteString( "!" ); WriteLn;
               WriteString( "!" ); WriteLn;
            (*– Imprimir el asiento –*)
               WriteString( "======" ); WriteLn;
            (*– Imprimir las patas –*)
               WriteString( "!         !" ); WriteLn;
               WriteString( "!         !" ); WriteLn;
 
En este ejemplo se han utilizado caracteres fácilmente visibles "–" para marcar el principio y el final de los comentarios-orden. Como el programa es corto, no se ha usado ningún elemento para marcar el final de la acción compuesta, que termina simplemente donde empieza la siguiente. En programas más complicados el final de la acción compuesta podría marcarse también con un comentario similar al situado al comienzo, por ejemplo:
 
             (*– Imprimir las patas –*)
                WriteString( "!          !" ); WriteLn;
                WriteString( "!          !" ); WriteLn;
             (*– Fin de imprimir las patas –*)
 
Finalmente mencionaremos la posibilidad de emplear comentarios al margen. Estos comentarios sirven para aclarar el significado de ciertas sentencias del programa, que pueden ser difíciles de interpretar al leerlas tal como aparecen escritas en el lenguaje de programación empleado. Una recomendación de estilo es situar estos comentarios hacia la parte derecha del listado, en las mismas líneas que las sentencias que se comentan, y alineados todos a partir de una posición fija, hacia el comienzo del tercio final.
 
Los comentarios al margen se utilizan muchísimo para explicar el significado de cada variable usada en un programa, poniéndolos en la misma línea en que se declaran. Por ejemplo:
 
             VAR
                 base, altura: REAL;                      (* dimensiones en cm *)
                 area: REAL;                                (* área en cm^2 *)
                 volumen: INTEGER;                      (* volumen en litros *)
                 dia: CARDINAL;                           (* entre 1 y 31 *)
 
Como resumen de todas las recomendaciones sobre el empleo de comentarios, se muestra a continuación el listado completo del programa de imprimir la silueta de la silla, incluyendo comentarios de todos los tipos indicados. En un programa tan sencillo como éste muchos de los comentarios resultan superfluos y de hecho hacen el programa un tanto engorroso, pero se han incluido para ilustrar la manera de presentar cada clase de comentario.
 
(*********************************************************
*
* Programa: SILLA
*
* Autor: J.A. Cerrada
*
* Descripción:
*   Este programa imprime en forma esquemática la
*   silueta de una silla usando caracteres
*   normales de la impresora.
*
**********************************************************)
MODULE Silla;
(*========================================================
 IMPORTACION Y DECLARACIONES DEL PROGRAMA
==========================================================*)
  FROM InOut IMPORT WriteString, WriteLn;
BEGIN
(*========================================================
 PARTE EJECUTABLE DEL PROGRAMA
==========================================================*)
  (*– Imprimir el respaldo –*)
   WriteString( "!" ); WriteLn;           (* parte superior *)
 WriteString( "!" ); WriteLn;             (* parte inferior *)
  (*– Imprimir el asiento –**)
 WriteString( "======" ); WriteLn;
  (*– Imprimir las patas –*)
 WriteString( "!    !"); WriteLn;          (* parte superior *)
 WriteString( "!    !"); WriteLn           (* parte inferior *)
END Silla.
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