REUTILIZACIÓN DE CÓDIGO: una arquitectura sencilla



Se va a mostrar en primer lugar una arquitectura de programa que muestra un menú y va admitiendo opciones del usuario hasta que este pulsa la tecla de salida ('Q'). El programa hace uso de utiles.c , que se ha descrito detalladamente en la página anterior .

Toda la información que manejan las funciones del programa se halla en un bloque señalado por g , que es una variable de tipo GlobRef (un puntero de una estructura de tipo Globales ). La estructura está definida en el archivo tipos.h . De este modo, el archivo main.c actúa como distribuidor: admite opciones y ejecuta las opciones asociadas (en el ejemplo, si se pulsa H aparece la ayuda, y si se pulsa S el programa concluye).
El programa básico hace uso de las funciones siguientes:
  1. arranque()
  2. parada()
  3. ayuda()
  4. salir()
Las tres primeras funciones están definidas en cda.h y cda.c . La función salir() está definida en main.c . Con esto concluye la relación de funciones empleadas por nuestra arquitectura: es sencilla, aunque no sofisticada porque la adaptación a una funcionalidad concreta requiere muchos cambios (algunos delicados) en el programa principal main.c .

Para dar una funcionalidad real a este programa, que no hace nada, pero lo hace muy bien, es preciso añadir al archivo tipos.h> otros archivos de encabezado. Estos archivos contienen funciones que se usan como herramientas (primitivas en la jerga informática). Nuestro programa siempre va a utilizar funciones que reciben como único argumento la variable g y son de tipo void .

Programa básico


main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "tipos.h"
#include "cda.h"

void salir(GlobRef g);

int main(int argc, char * argv[])
{
  GlobRef g = malloc(sizeof(Globales));
  char respuesta;
  /* *** El orden de las opciones y de las funciones debe coincidir!!! *** */
  charRef  menu            = "(H) Ayuda (S) Salir";
  charRef  opciones        = "HS";
  funRef   funciones[]     = { ayuda, salir, NULL };

  arranque(g, argc, argv);
  do
    {
      printf("%s ",menu);
      respuesta = toupper(abs(getch()));

      switch(respuesta) {
        case 'H': funciones[0](g); break;
        case 'S': funciones[1](g); break;
        default:  printf("\n\n\7***Opción incorrecta\n\n"); break;
      }
    } while(!g->salirDePrograma);

  parada(g);
  printf("\n\nTerminación normal\n\n");
  return 0;
}

void salir(GlobRef g)
{
  g->salirDePrograma = confirmar("¿Desea realmente salir del programa? ");
}
tipos.h
#include <coti/utiles.h>

#ifndef __TIPOS__
#define __TIPOS__
/*
  Normalmente, será preciso incluir aquí los archivos de encabezado
  que aportan tipos específicos para la funcionalidad deseada.
 */

typedef struct Globales {

  int salirDePrograma;
} Globales;

typedef Globales * GlobRef;

typedef void (*funRef)(GlobRef);

#endif
cda.h
#include <coti/utiles.h>
#include "tipos.h"

void arranque(GlobRef g, int argc, char * argv[]);
void parada  (GlobRef g);
void ayuda   (GlobRef g);
cda.c
#include "cda.h"
/*
  Constructor, destructor y ayuda
  -            -            -
 */
void arranque(GlobRef g, int argc, charRef argv[])
{
  printf("\n\nSe ha ejecutado el procedimiento de arranque.\n\n");
  g->salirDePrograma = 0; // No se va a salir en el momento de la entrada */
}

void parada(GlobRef g)
{
  printf("\n\nSe ha ejecutado el procedimiento de parada.\n\n");
}

void ayuda(GlobRef g)
{
  printf("\n\nEste es el mecanismo de ayuda.\n\n");
}

makefile

Para simplificar la compilación se puede utilizar el siguiente archivo makefile , que permite crear el archivo de código ejecutable escribiendo simplemente make en la línea de órdenes.

#
# makefile general para compilar un programa
#
# Las variables terminadas en 64 deben utilizarse para
# efectuar compilaciones en 64 bits.
#
# Última modificación: 2009 09 06
#
OBJECTS=cda.o main.o
OBJECTS64=cda64.o main64.o
UTILES=/usr/include/coti/utiles.o
UTILES64=/usr/include/utiles64.o
CC=gcc
PROGRAM=program
#PROGRAM=program.exe
RM=rm -rf
#RM=del

program: $(OBJECTS)
  $(CC) $(UTILES) $(OBJECTS) -o program

cda.o: cda.c
  $(CC) -c cda.c

main.o: main.c
  $(CC) -c main.c

clean:
  $(RM) *.o
  $(RM) $(PROGRAM)

Un ejemplo de programa con esta arquitectura.

Se trata de emplear la arquitectura anterior para construir un programa que haga algo sencillo, como pedir dos números, calcular su suma y mostrar el resultado (empleando la arquitectura propuesta). Se han marcado en verde las modificaciones.

main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "tipos.h"
#include "cda.h"

void salir(GlobRef g);

int main(int argc, char * argv[])
{
  GlobRef g = malloc(sizeof(Globales));
  char respuesta;
  /* El orden de las opciones y de las funciones debe coincidir !!! */

  charRef  menu        = "(1) Escribir números (2) Calcular"
                         " (3) Ver resultados (H) Ayuda (S) Salir";
  charRef  opciones    = "123HS";
  funRef   funciones[] = {leer_datos,calcular,mostrar_informacion,ayuda, salir, NULL};

  arranque(g, argc, argv);
  do
    {
      printf("%s ",menu);
      respuesta = toupper(abs(getch()));
      /* Hay que adaptar el switch. */
      switch(respuesta) {

        case '1': funciones[0](g); break;
        case '2': funciones[1](g); break;
        case '3': funciones[2](g); break;
        case 'H': funciones[3](g); break;
        case 'S': funciones[4](g); break;
       default: printf("\n\n\7***Opción incorrecta\n\n"); break;
      }
    } while(!g->salirDePrograma);

  parada(g);
  printf("\n\nTerminación normal\n\n");
  return 0;
}

void salir(GlobRef g)
{
  printf("\n\n");
  g->salirDePrograma = confirmar("¿Desea realmente salir del programa? ");
}
tipos.h
#include <coti/utiles.h>

#ifndef __TIPOS__
#define __TIPOS__
/* Archivos de encabezado adicionales */

#include "funciones.h"


typedef struct Globales {

  int primerSumando,segundoSumando,resultado;

  int salirDePrograma;
} Globales;

typedef Globales * GlobRef;

typedef void (*funRef)(GlobRef);

#endif
cda.h
#include <coti/utiles.h>
#include "tipos.h"

void arranque(GlobRef g, int argc, char * argv[]);
void parada  (GlobRef g);
void ayuda   (GlobRef g);
cda.c
#include "cda.h"
/* Constructor, destructor y ayuda */
void arranque(GlobRef g, int argc, charRef argv[])
{
  printf("\n\nSe ha ejecutado el procedimiento de arranque.\n\n");
  g->salirDePrograma = 0; // No se va a salir en el momento de la entrada */
}

void parada(GlobRef g)
{
  printf("\n\nSe ha ejecutado el procedimiento de parada.\n\n");
}

void ayuda(GlobRef g)
{
  printf("\n\nEste es el mecanismo de ayuda.\n\n");
}
funciones.h

#include "tipos.h"

void leer_datos(GlobRef);
void calcular(GlobRef);
void mostrar_informacion(GlobRef);

funciones.c

#include "funciones.h"

void leer_datos(GlobRef g)
{
  printf("\n\n");
  g->primerSumando  = pedirInt("Escriba el primer número  : ",-2147000000,2147000000);
  g->segundoSumando = pedirInt("Escriba el segundo número : ",-2147000000,2147000000);
  printf("\n\n");
}

void calcular(GlobRef g)
{
  g->resultado = g->primerSumando + g->segundoSumando;
  printf("\n\nCálculos efectuados\n\n");
}

void mostrar_informacion(GlobRef g)
{
  printf("\n\nLa suma de %d y %d es %d\n\n",
    g->primerSumando,
    g->segundoSumando,
    g->resultado);
}

Conclusiones

La adaptación de esta arquitectura es relativamente sencilla:
  1. Se añaden las variables necesarias ( primerSumando , segundoSumando y resultado ) a la estructura Globales . Dicho de otro modo, se añaden a Globales las estructuras de datos necesarias para el funcionamiento de la aplicación. Este paso es fundamental: si no se crean unas estructuras de datos, no se puede efectuar el tratamiento de información.
  2. Se añade en tipos.h el archivo de encabezado funciones.h . Si hubiera más archivos de encabezado, se añaden también aquí.
  3. Se modifican las variables menu , opciones y funciones . Obsérvese que las opciones "123HS" tienen el mismo orden que las funciones asociadas { leer_datos, calcular, mostrar_informacion, ayuda, salir } . No hay que olvidar el NULL que marca el final de la lista de funciones. Nota: ciertamente, se podrían escribir directamente los nombres de las funciones en el switch() . La razón por la cual almacenamos los punteros de función en una lista se comprenderá en la próxima arquitectura de programa que se estudia.
  4. Se modifica por completo el contenido de la sentencia switch() . Concretamente, se hace lo siguiente:
  5. Basándose en los nombres de funciones que se han utilizado al principio del programa, se crean los archivos funciones.h y funciones.c , que aportan la funcionalidad deseada. En las funciones, que reciben todas ellas la variable g, se efectuarán posiblemente llamadas a subprogramas pertenecientes a otras bibliotecas predefinidas o creadas para este programa en concreto.
El principal problema que plantea esta arquitectura plantea es la complejidad asociada a modificar la sentencia switch() . Una arquitectura ideal solo precisaría modificar el menú, las opciones y la lista de funciones asociadas.

Ejercicios

  1. Tomando como base el makefile propuesto, crear dos versiones llamadas makefile_unix y makefile_windows . Cada una debe servir únicamente para la plataforma correspondiente. Comprobar que ambas funcionan.
  2. Crear una