Gestión de Datos mediante Composición y Patrón Dirty Flag
La composición es una técnica fundamental en la programación orientada a objetos donde una clase contiene instancias de otras clases como miembros. En este ejemplo, diseñamos un evaluador de calificaciones que utiliza contenedores de la biblioteca estándar (STL) para gestionar datos y un indicador de estado para optimizar cálculos.
// EvaluadorAcademico.hpp
#include <string>
#include <vector>
#include <array>
class EvaluadorAcademico {
public:
EvaluadorAcademico(const std::string& nombre_materia);
void registrarNotas(int cantidad);
void mostrarResultados() const;
void ejecutarEstadisticas();
void ordenar(bool descendente = true);
private:
void calcular();
std::string materia;
std::vector<int> lista_notas; // Composición: almacena los datos brutos
std::array<int, 5> distribucion; // Composición: conteo por rangos
std::array<double, 5> porcentajes;
bool requiere_recalculo; // Dirty Flag
};
El uso de un Dirty Flag (requiere_recalculo) permite que el método calcular() solo se ejecute cuando los datos han cambiado, evitando procesamientos redundantes si se solicita la información estadística múltiples veces sin modificar las notas de entrada.
// EvaluadorAcademico.cpp (Fragmento de lógica)
void EvaluadorAcademico::registrarNotas(int n) {
if (n <= 0) return;
lista_notas.reserve(n); // Optimización de memoria
int valor;
for (int i = 0; i < n; ++i) {
std::cin >> valor;
if (valor >= 0 && valor <= 100) {
lista_notas.push_back(valor);
}
}
requiere_recalculo = true;
}
void EvaluadorAcademico::calcular() {
if (!requiere_recalculo) return;
distribucion.fill(0);
for (int n : lista_notas) {
if (n < 60) distribucion[0]++;
else if (n < 70) distribucion[1]++;
else if (n < 80) distribucion[2]++;
else if (n < 90) distribucion[3]++;
else distribucion[4]++;
}
for (size_t i = 0; i < distribucion.size(); ++i) {
porcentajes[i] = static_cast<double>(distribucion[i]) / lista_notas.size();
}
requiere_recalculo = false;
}
Análisis de Diseño y Rendimiento
- Encapsulamiento: Los miembros como
lista_notasson privados. Intentar acceder a ellos directamente desde elmain(ej.obj.lista_notas.push_back(10)) generará un error de compilación, protegiendo la integridad de los datos. - Optimización con reserve(): El método
reserve()preasigna memoria en el vector. Sin esto, el vector podría reasignar memoria múltiples veces durante la inserción, lo que implica copiar elementos y reducir el rendimiento en conjuntos de datos grandes. - Cálculo de Mediana: Para extender la funcionalidad sin agregar miembros, se puede implementar un método que cree una copia local de los datos, los ordene y devuelva el valor central.
Diferencias entre Composición y Herencia Privada
Es posible implementar la misma funcionalidad heredando de forma privada de std::vector<int>. Sin embargo, la composición suele preferirse porque define una relación clara de "tiene un", mientras que la herencia privada se utiliza más para reutilizar la implementación sin exponer la interfaz de la clase base.
// Ejemplo de Herencia Privada
class EvaluadorV2 : private std::vector<int> {
public:
void agregar(int v) { this->push_back(v); }
// Los métodos de vector no son accesibles externamente
};
Polimorfismo Dinámico en Sistemas Gráficos
El uso de funciones virtuales permite tratar diferentes objetos a través de un puntero a una clase base común. Esto es esencial para sistemas extensibles como un lienzo de dibujo.
// Estructura de Clases para Gráficos
class Forma {
public:
virtual void renderizar() const = 0; // Función virtual pura
virtual ~Forma() = default; // Destructor virtual obligatorio
};
class Elipse : public Forma {
public:
void renderizar() const override { std::cout << "Renderizando elipse..." << std::endl; }
};
class Cuadrado : public Forma {
public:
void renderizar() const override { std::cout << "Renderizando cuadrado..." << std::endl; }
};
class Panel {
public:
void insertar(Forma* f) { elementos.push_back(f); }
void dibujarTodo() const {
for (auto e : elementos) e->renderizar();
}
~Panel() {
for (auto e : elementos) delete e;
}
private:
std::vector<Forma*> elementos;
};
En este diseño, el destructor virtual es crítico. Si la clase base Forma no tuviera un destructor virtual, al ejecutar delete e solo se llamaría al destructor de Forma, ignorando los destructores de las clases derivadas (como Elipse), lo que resultaría en fugas de memoria si estas últimas gestionan recursos propios.
Simulación de Fábrica de Objetos medianet Abstracción
Combinando herencia y composición, podemos modelar un sistema de catálogo donde una clase gestora administra diversos objetos concretos derivados de una base abstracta.
class Producto {
protected:
std::string id;
public:
Producto(const std::string& _id) : id(_id) {}
virtual void ejecutarAccion() const = 0;
virtual ~Producto() = default;
};
class Sensor : public Producto {
public:
Sensor(const std::string& _id) : Producto(_id) {}
void ejecutarAccion() const override {
std::cout << "Sensor [" << id << "]: Escaneando entorno." << std::endl;
}
};
class Actuador : public Producto {
public:
Actuador(const std::string& _id) : Producto(_id) {}
void ejecutarAccion() const override {
std::cout << "Actuador [" << id << "]: Activando mecanismo." << std::endl;
}
};
class GestorDispositivos {
std::vector<Producto*> inventario;
public:
void registrar(Producto* p) { inventario.push_back(p); }
void procesar() {
for (auto p : inventario) p->ejecutarAccion();
}
~GestorDispositivos() {
for (auto p : inventario) delete p;
}
};
Este enfoque permite que el GestorDispositivos permanezca agnóstico a los tipos específicos de productos. Si se añade un nuevo tipo de dispositivo, solo es necesario derivarlo de Producto e implementar ejecutarAccion(), cumpliendo con el principio de Abierto/Cerrado (Open/Closed Principle).