REUTILIZACIÓN - Módulos de una arquitectura reutilizable


Los archivos de que consta el programa son los siguientes:
  1. main.c
  2. aplicacion.h
  3. aplicacion.c
  4. cda.h
  5. cda.c
  6. tipos.h


Véase a continuación el contenido de estos archivos:

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

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

int main(int argc, char * argv[])
{
  GlobRef g = malloc(sizeof(Globales));

  /* Sólo hay que modificar las variables siguientes. */
  charRef  menu            = "(H) Ayuda (S) Salir";
  charRef  opciones        = "HS";
  funRef   funciones[]     = {  ayuda, salir, NULL};

  /* El resto del programa no se modifica */

  arranque(g, argc, argv);
  distribuidor(g,menu,opciones,funciones);
  parada(g);
  printf("\n\nTerminación normal\n\n");
  return 0;
}
aplicacion.h
#include <coti/utiles.h>
#include "tipos.h"

void distribuidor(  GlobRef g,
                    charRef men,
                    charRef opc,
                    funRef * fun);
void salir      (   GlobRef g);
void volver     (   GlobRef g);
aplicacion.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "aplicacion.h"

/* Esta parte del programa es fija, no requiere modificaciones. */

void salir(GlobRef g)
{
    g->salirDePrograma = confirmar("\n\n¿Desea salir del programa?");
}

void volver(GlobRef g)
{
  printf("\n\n***\7Opción incorrecta***\n\n");
  return;
}

void distribuidor(  GlobRef  g,
                    charRef  men,
                    charRef  opc,
                    funRef * fun)
{
  char buf[16];
  char orden;
  int numOpciones = 0;
  int numFunciones = 0;
  int i;
  /*  Nueva lista de funciones, con orden distinto al dado inicialmente */
  funRef * f;
  /* Se determina el número de opciones */
  numOpciones = strlen(opc);
  /*  Se determina el número de funciones */
  while(fun[numFunciones]) numFunciones++;
  /* Si numOpciones y numFunciones no coinciden, error irrecuperable */
  if (numOpciones != numFunciones)
  {
    printf("\n\nError fatal: numOpciones (%d) =! numFunciones (%d)\n\n",
         numOpciones, numFunciones);
    exit(1);
  }
  /*
   Si todo va bien, se crea una nueva estructura de datos
   para simplificar la ejecución. Se puede pensar que se trata
   de una tabla dispersiva (véase Hash Table) muy primitiva.

   El mecanismo consiste en crear una tabla formada por 256 posibles
   valores de tipo pfuncion. En cada posición de la tabla se almacena
   originalmente la dirección volver.
   */
  f = malloc(256*sizeof(funRef));
  for(i=0;i<256;i++) f[i] = volver;
  /*
   A continuación, se recorre la lista de opciones correspondientes
   a funciones. Cada opción es un número. Se utiliza ese número como índice
   dentro de la lista de funciones, y en la posición correspondiente
   se almacena el puntero de función asociado a esa opción.

   Según se ha construido la aplicación, opc[i] es la
   i-ésima opción y tiene asociada fun[i]:
opc[0] es 'H' y fun[0] es ayuda()
opc[1] es 'S' y fun[1] es salir()
En el siguiente bucle for() se ejecuta la instrucción f[opc[i]] = fun[i], para i=0..numOpciones-1. Entonces se está haciendo lo siguiente:
f[opc[0]] = fun[0]
f[opc[1]] = fun[1]
Por tanto, f['H'] toma como valor la dirección de ayuda() y por f['S'] toma como valor la dirección de salir(). Hemos preparado las cosas para que todo funcione bien. */ for(i=0;i<numOpciones;i++) f[opc[i]] = fun[i]; /************************************************************ Ahora la opción pulsada por el usuario es directamente el índice de la función seleccionada *************************************************************/ do { printf("%s ", men); orden = toupper(abs(getch())); f[orden](g); } while (!g->salirDePrograma); free(f); return 0; }
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");
}
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

Comentarios

main.c

Este programa resulta extraordinariamente breve (contiene 5 sentencias de código ejecutable) y define la interfaz de usuario del programa.

menu
En la definición del menú deben incluirse expresiones de la forma siguiente:
(letra equivalente a la opción) Opción de menú
por ejemplo,
charRef  menu = "(H) Ayuda (S) Salir";
Este menú se mostrará sin un solo cambio en pantalla.

opciones
Esta cadena contiene las letras equivalentes definidas anteriormente en el menú, y por el mismo orden. Para el menú anterior, se pondría:
charRef  opciones = "HS";
Como puede observarse, nos hemos limitado a recopilar las letras H y S que aparecían como equivalentes de Ayuda y Salir en el menú.

funciones
Es una lista formada por los nombres de las funciones que van a ser invocadas cuando el usuario pulse una de las opciones definidas en los dos apartados anteriores. Evidentemente, esas funciones deben haber sido definidas con anterioridad, normalmente en los encabezados que se incluyen inmediatamente después de aplicacion.h . Para la combinación de menu y opciones anterior se pondría lo siguiente:
funRef funciones[] = {   ayuda, salir, NULL};
Téngase en cuenta que las funciones ayuda() y salir() están definidas en el archivo de encabezado cda.h . Por supuesto, podrían estar definidas en cualquier otro archivo de encabezado adicional.

arranque() Recibe como argumentos a las variables argc , argv y f . Por tanto tiene acceso a los argumentos de la línea de órdenes, y a la totalidad de la información del programa. Sirve para efectuar las iniciaciones necesarias; viene a ser un constructor (véase el oportuno concepto de POO). Deberá modificarse para adaptarla a la funcionalidad del programa construido.

distribuidor() Recibe como argumentos las variables g , menu , opciones y funciones . Sirve para mostrar el menú, pedir opciones al usuario y ejecutar las funciones correspondientes a esas funciones. Esta función es independiente del programa construido; en principio, nunca a va a sufrir cambios.

parada() Recibe como argumento la variable g . Por tanto, tiene acceso a toda la información del programa. Se ejecuta inmediatamente antes de la conclusión del mismo, y sirve para llevar a cabo las últimas operaciones necesarias para el correcto arranque en la próxima ejecución. Deberá modificarse para adaptarla a la funcionalidad del programa construido. Una posible mejora consiste en hacer que devuelva el entero que proporciona finalmente el programa, de forma similar a esta:
return parada(g);

aplicacion.h

Contiene los prototipos de distribuidor() , salir() y volver() . No debería sufrir modificaciones.

aplicacion.c

Contiene los cuerpos de distribuidor() , salir() y volver() . No debería sufrir modificaciones.

salir() Pregunta al usuario si desea o no salir del programa; en caso afirmativo da el valor 1 al campo salirDePrograma de la estructura señalada por g .

volver() Emite un pitido ('\7'), muestra un mensaje de opción incorrecta y retorna.

distribuidor() Es la función principal de este programa reutilizable. El aspecto crucial es utilizar la letra pulsada por el usuario como índice de una lista de funciones; a cada letra de opciones se le asigna una posición de la lista de punteros de función que contiene la dirección de la función que debe ejecutarse al pulsar esa tecla. Se parte de una lista de opciones y otra de punteros de función, en posiciones homólogas (el índice de una letra es el índice de la función asociada). Se construye una segunda lista de funciones en que el valor de la letra es el índice de la función asociada, mediante
f[opc[i]] = fun[i]
Esto da lugar al siguiente bucle de procesamiento
do
    {
        printf("%s ", men);
        orden = toupper(abs(getch()));
        f[orden](g);
    } while (!g->salirDePrograma);
Cuando el usuario pulsa una tecla, se ejecuta la función correspondiente.

cda.h

Contiene los prototipos de arranque(), parada() y ayuda(). No debería sufrir modificaciones.

cda.c

Contiene las definiciones de arranque() , parada() y ayuda() . Estas tres funciones deben adaptarse a la funcionalidad deseada.

tipos.h

Es una parte crucial de este programa reutilizable. Globales contiene en sus campos toda la información del programa. Debe modificarse esta estructura de datos para adaptarla a la funcionalidad que quiera darse al programa. A través de su puntero, todas las funciones invocadas por el usuario pueden acceder a toda la información del programa.

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). La relación de archivos de que consta este programa es como sigue:
  1. tipos.h
  2. main.c
  3. makefile
  4. funciones.h
  5. funciones.c
  6. cda.h (sin cambios)
  7. cda.c (sin cambios)
  8. aplicacion.h (sin cambios)
  9. aplicacion.c (sin cambios)
Evidentemente, para construirlo es preciso situar en una carpeta todos los archivos anteriores. Esto es rápido: se duplica la carpeta que contiene todos los archivos del primer programa descrito en esta página y se añaden funciones.h y funciones.c. Después se hacen las modificaciones que se indican en los tres archivos siguientes; se han marcado en verde los cambios necesarios.

tipos.h
#include <coti/utiles.h>

#ifndef __TIPOS__
#define __TIPOS__

/* Incluir aquí los archivos aportan tipos
específicos para la funcionalidad deseada. */


typedef struct Globales {
  int primerSumando, segundoSumando, resultado;
  int salirDePrograma;
} Globales;

typedef Globales * GlobRef;

typedef void (*funRef)(GlobRef);

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

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

/* Prototipos que deben figurar en funciones[]. */

#include "funciones.h"

int main(int argc, char * argv[])
{
  GlobRef g = malloc(sizeof(Globales));

  /* Valores de menu, opciones y funciones */
  charRef  menu  = "(X) Leer (Y) Calcular (Z) Ver suma "
                   "(H) Ayuda (S) Salir";
  charRef  opciones  = "XYZHS";
  funRef   funciones[]  = { leer_datos, calcular,
  mostrar_informacion, ayuda, salir, NULL};

  /* Zona sin cambios */

  arranque(g, argc, argv);
  distribuidor(g,menu,opciones,funciones);
  parada(g);
  printf("\n\nTerminación normal\n\n");
  return 0;
}
makefile
#
# makefile general para compilar un programa
#
# Última modificación: 2009 09 11
#
OBJECTS=aplicacion.o cda.o main.o funciones.o
UTILES=/usr/include/coti/utiles.o
CC=gcc
PROGRAM=program
#PROGRAM=program.exe
RM=rm -rf
#RM=del

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

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

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

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

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

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

Comentarios

Los cambios necesarios para adaptar esta arquitectura a la funcionalidad que se deseaba (sumar dos números) son realmente muy pocos:
  1. En tipos.h se añaden las variables necesarias a Globales .
  2. En main.c se incluye funciones.h y se modifica el menú, la lista de opciones y la lista de funciones invocadas cuando se pulsa una de esas opciones.
  3. En makefile se añade a OBJECTS el archivo de código objeto, funciones .o, y se añade una regla de creación para funciones .o.
¡Eso es todo! En comparación con la arquitectura anterior, el número y tipo de cambios es muy inferior. Por esta razón, se utilizará de forma extensa a lo largo del curso la arquitectura que se muestra al principio de esta página; la arquitectura anterior se descarta por excesivamente compleja a la hora de crear nuevos programas.