Patrones de Abstracción, Polimorfismo y Máquinas de Estado en C

Para implementar sistemas robustos en C, es imperativo trascender la simple manipulación de datos y adoptar mecanismos de abstracción que permitan el encapsulamiento y el polimorfismo. El primer pilar es el tipo opaco (o opaque type). Al declarar una estructura en el encabezado mediante un typedef struct Sensor Sensor; sin definir sus campos, creamos un “handle” que el cliente puede manipular mediante punteros, pero cuyo contenido permanece oculto. Esto garantiza que el cliente no pueda corromper el estado interno y permite modificar la estructura en la implementación sin invalidar la ABI (Application Binary Interface), evitando recompilaciones innecesarias.

El segundo pilar es la vtable (tabla de funciones virtuales). Dado que C no posee una tabla de métodos integrada, simulamos el comportamiento polimórfico mediante una estructura que contiene punteros a funciones. Al invocar un método, el programa realiza un salto indirecto a la implementación específica de la instancia actual. Este mecanismo requiere la herencia por composición: para que un puntero a una estructura “hija” pueda ser tratado como un puntero a la estructura “base” de forma segura (upcasting), la estructura base debe ser el primer miembro de la estructura derivada, asegurando que ambas compartan la misma dirección de memoria inicial.

Finalmente, la gestión de la lógica de negocio compleja se delega a una máquina de estados. Esta se implementa típicamente mediante un enum que representa los estados posibles y una lógica de transición que puede ser una tabla de punteros a función o un bloque switch que reacciona a eventos, asegurando que el objeto solo sea válido en transiciones permitidas.

Si se confunde el orden de los miembros en la composición o se realiza un casting de un puntero que no tiene la base como primer elemento, el programa accederá a memoria inválida, provocando un fallo de segmentación o, peor aún, un comportamiento indefinido silencioso. Si no se inicializa la vtable en el constructor de la implementación concreta, cualquier llamada a un método derivará en un salto a una dirección de memoria nula o basura.

#include <stdio.h>
#include <stdlib.h>

/* --- Interfaz (Header simulado) --- */

typedef enum {
    STATE_IDLE,
    STATE_BUSY,
    STATE_ERROR
} SensorState;

// Forward declaration para el tipo opaco
typedef struct Sensor Sensor;

// La vtable define el contrato de la interfaz
typedef struct {
    int (*read)(Sensor *self);
    void (*destroy)(Sensor *self);
} SensorVTable;

// La estructura base que define la interfaz y el estado
struct Sensor {
    const SensorVTable *vtable;
    SensorState state;
};

// Funciones de la API pública (el cliente solo ve 'Sensor*')
int sensor_read(Sensor *s);
void sensor_destroy(Sensor *s);

/* --- Implementación --- */

// Estructura concreta: Herencia por composición
typedef struct {
    Sensor base;       // Debe ser el primer miembro para permitir upcasting seguro
    float temperature; // Dato específico de la implementación
} Thermometer;

// Implementación de la función 'read' para Thermometer
static int thermometer_read(Sensor *self) {
    // Upcasting seguro: el puntero a base es igual al puntero a la estructura completa
    Thermometer *therm = (Thermometer *)self;

    if (self->state == STATE_ERROR) {
        fprintf(stderr, "[Error] Sensor en estado de error.\n");
        return -1;
    }

    // Transición de estado: Máquina de estados simple
    self->state = STATE_BUSY;
    
    printf("[Log] Leyendo temperatura...\n");
    therm->temperature += 0.5f; // Simulación de lectura
    
    self->state = STATE_IDLE;
    printf("[Log] Lectura exitosa: %.2f C\n", therm->temperature);
    return 0;
}

// Implementación de la liberación de memoria
static void thermometer_destroy(Sensor *self) {
    printf("[Log] Liberando memoria de Termómetro...\n");
    free(self);
}

// La vtable estática para Thermometer
static const SensorVTable THERMOMETER_VTABLE = {
    .read = thermometer_read,
    .destroy = thermometer_destroy
};

// Constructor de la implementación concreta
Sensor *thermometer_create(void) {
    Thermometer *therm = malloc(sizeof(Thermometer));
    if (!therm) return NULL;

    therm->base.vtable = &THERMOMETER_VTABLE;
    therm->base.state = STATE_IDLE;
    therm->temperature = 20.0f;
    
    return (Sensor *)therm;
}

/* --- Implementación de la API Pública --- */

int sensor_read(Sensor *s) {
    if (!s || !s->vtable->read) return -1;
    return s->vtable->read(s);
}

void sensor_destroy(Sensor *s) {
    if (s && s->vtable->destroy) {
        s->vtable->destroy(s);
    }
}

/* --- Driver de prueba --- */

int main(void) {
    Sensor *my_sensor = thermometer_create();
    if (!my_sensor) return EXIT_FAILURE;

    // El cliente usa la interfaz 'Sensor', ignorando que es un termómetro
    for (int i = 0; i < 3; i++) {
        sensor_read(my_sensor);
    }

    // Forzamos un error para probar la máquina de estados
    my_sensor->state = STATE_ERROR;
    if (sensor_read(my_sensor) != 0) {
        printf("[Main] El sensor falló como se esperaba.\n");
    }

    sensor_destroy(my_sensor);
    return EXIT_SUCCESS;
}

Análisis de la implementación

El programa utiliza un tipo opaco mediante el puntero Sensor * en el main. El cliente no tiene acceso a therm->temperature ni a la estructura Thermometer, cumpliendo con el principio de encapsulamiento.

La magia del polimorfismo ocurre en sensor_read. Esta función no sabe qué tipo de sensor está manejando; simplemente accede a s->vtable->read(s). Aquí, el puntero s se pasa como argumento para actuar como el equivalente al this de C++. Es crucial notar que thermometer_read realiza un downcast de Sensor * a Thermometer *. Esto es seguro porque base es el primer miembro de Thermometer, lo que garantiza que la dirección de memoria de base y de therm sea idéntica.

La máquina de estados es gestionada dentro de thermometer_read mediante el campo state. Antes de realizar la operación, verificamos si el estado es STATE_ERROR, y durante la operación, transitamos a STATE_BUSY. Este control es vital para evitar condiciones de carrera en sistemas embebidos o acceso a hardware mientras está ocupado.

El uso de una vtable estática (THERMOMETER_VTABLE) es una optimización de memoria clásica: todos los objetos de tipo Thermometer comparten la misma tabla de funciones, evitando duplicar punteros de función en cada instancia.

El error frecuente

Un error común en este patrón es el “constructor incompleto”. Si un programador implementa una nueva estructura derivada pero olvida asignar la vtable en el constructor:

// ERROR: Olvido de la vtable
Thermometer *bad_therm = malloc(sizeof(Thermometer));
bad_therm->base.state = STATE_IDLE; 
// Faltó: bad_therm->base.vtable = &THERMOMETER_VTABLE;

sensor_read((Sensor *)bad_therm); // ¡BOOM! Crash por salto a dirección NULL

Este error es difícil de detectar con herramientas básicas porque la memoria está asignada, pero el puntero de función es NULL o contiene basura. Herramientas como AddressSanitizer detectarán el acceso a memoria inválida, pero el error lógico reside en la omisión de la inicialización del objeto.

134

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio