Ir al contenido

Composicion, Interfaces y Diseno

Domina la composicion sobre la herencia, las interfaces abstractas puras, los principios de diseno (ISP, DIP), std::variant como alternativa a la herencia, y la eliminacion de tipo.

Por Que Composicion Sobre Herencia?

"Favorecer la composicion sobre la herencia" es uno de los principios mas importantes en el diseno POO. La herencia crea un acoplamiento fuerte entre clases base y derivadas — los cambios en la base se propagan por toda la jerarquia. La composicion (tener un miembro de otro tipo) es mas flexible: puedes cambiar el tipo contenido en tiempo de ejecucion, combinar comportamientos libremente y evitar el problema de la clase base fragil.

La herencia es apropiada cuando necesitas polimorfismo (tratar diferentes tipos de manera uniforme a traves de una interfaz comun). Solo para reutilizacion de codigo, la composicion es casi siempre superior.

Composicion en la Practica

En lugar de heredar comportamiento, contenlo. Un Car no es un Engine — tiene un Engine. Esto te da libertad para intercambiar motores, probar componentes independientemente y evolucionar cada clase sin romper la otra.

composition.cpp
#include <iostream>
#include <string>
#include <memory>

class Engine {
    int horsepower_;
    std::string type_;

public:
    Engine(int hp, const std::string& type)
        : horsepower_{hp}, type_{type} {}

    void start() const {
        std::cout << type_ << " engine (" << horsepower_ << "hp) started\n";
    }

    int horsepower() const { return horsepower_; }
};

class Transmission {
    int gears_;
public:
    explicit Transmission(int gears) : gears_{gears} {}
    void shift(int gear) const {
        if (gear >= 1 && gear <= gears_) {
            std::cout << "Shifted to gear " << gear << '\n';
        }
    }
};

// Car COMPOSES Engine and Transmission (has-a, not is-a)
class Car {
    std::string model_;
    Engine engine_;
    Transmission trans_;

public:
    Car(const std::string& model, Engine engine, Transmission trans)
        : model_{model}
        , engine_{std::move(engine)}
        , trans_{std::move(trans)} {}

    void drive() const {
        std::cout << model_ << ": ";
        engine_.start();
        trans_.shift(1);
    }
};

int main() {
    Engine v8{450, "V8"};
    Transmission auto6{6};
    Car mustang{"Mustang", std::move(v8), std::move(auto6)};
    mustang.drive();
    return 0;
}

Interfaces Abstractas Puras

Una interfaz abstracta pura es una clase con solo funciones virtuales puras y un destructor virtual — sin datos, sin implementacion. Define un contrato que las clases derivadas deben cumplir. Soporta el Principio de Inversion de Dependencias (DIP): depende de abstracciones, no de clases concretas.

interface.cpp
#include <iostream>
#include <memory>
#include <string>
#include <vector>

// Pure abstract interface — the contract
class IRepository {
public:
    virtual ~IRepository() = default;
    virtual void save(const std::string& key, const std::string& value) = 0;
    virtual std::string load(const std::string& key) const = 0;
    virtual bool exists(const std::string& key) const = 0;
};

// Concrete implementation: in-memory storage
class MemoryRepository : public IRepository {
    std::vector<std::pair<std::string, std::string>> data_;

public:
    void save(const std::string& key, const std::string& value) override {
        for (auto& [k, v] : data_) {
            if (k == key) { v = value; return; }
        }
        data_.emplace_back(key, value);
    }

    std::string load(const std::string& key) const override {
        for (const auto& [k, v] : data_) {
            if (k == key) return v;
        }
        return "";
    }

    bool exists(const std::string& key) const override {
        for (const auto& [k, v] : data_) {
            if (k == key) return true;
        }
        return false;
    }
};

// Service depends on the INTERFACE, not the concrete class (DIP)
class UserService {
    std::unique_ptr<IRepository> repo_;

public:
    explicit UserService(std::unique_ptr<IRepository> repo)
        : repo_{std::move(repo)} {}

    void register_user(const std::string& username) {
        if (repo_->exists(username)) {
            std::cout << username << " already exists\n";
            return;
        }
        repo_->save(username, "active");
        std::cout << username << " registered\n";
    }
};

int main() {
    auto repo = std::make_unique<MemoryRepository>();
    UserService service{std::move(repo)};

    service.register_user("alice");  // alice registered
    service.register_user("bob");    // bob registered
    service.register_user("alice");  // alice already exists
    return 0;
}

std::variant — Alternativa a la Herencia

std::variant es una union segura para tipos que puede contener uno de varios tipos. Combinado con std::visit, proporciona polimorfismo en tiempo de compilacion — una alternativa a las funciones virtuales cuando el conjunto de tipos se conoce en tiempo de compilacion. Evita la asignacion en el monton y la sobrecarga del despacho virtual.

variant_poly.cpp
#include <iostream>
#include <variant>
#include <vector>
#include <cmath>
#include <numbers>

struct Circle  { double radius; };
struct Square  { double side; };
struct Triangle { double base, height; };

// Type alias for the closed set of shapes
using Shape = std::variant<Circle, Square, Triangle>;

// Visitor: a callable that handles each type
struct AreaVisitor {
    double operator()(const Circle& c) const {
        return std::numbers::pi * c.radius * c.radius;
    }
    double operator()(const Square& s) const {
        return s.side * s.side;
    }
    double operator()(const Triangle& t) const {
        return 0.5 * t.base * t.height;
    }
};

struct NameVisitor {
    std::string operator()(const Circle&) const { return "Circle"; }
    std::string operator()(const Square&) const { return "Square"; }
    std::string operator()(const Triangle&) const { return "Triangle"; }
};

int main() {
    std::vector<Shape> shapes{
        Circle{5.0},
        Square{4.0},
        Triangle{3.0, 6.0}
    };

    for (const auto& shape : shapes) {
        std::string name = std::visit(NameVisitor{}, shape);
        double area = std::visit(AreaVisitor{}, shape);
        std::cout << name << ": area = " << area << '\n';
    }
    return 0;
}

Eliminacion de Tipo con std::function

La eliminacion de tipo te permite almacenar cualquier callable — funcion, lambda, functor — con una firma compatible en un unico objeto std::function. Esta es una forma de polimorfismo en tiempo de ejecucion sin herencia explicita.

type_erasure.cpp
#include <iostream>
#include <functional>
#include <vector>
#include <string>

// std::function erases the concrete callable type
using Callback = std::function<void(const std::string&)>;

class EventEmitter {
    std::vector<Callback> listeners_;

public:
    void on(Callback cb) {
        listeners_.push_back(std::move(cb));
    }

    void emit(const std::string& event) const {
        for (const auto& cb : listeners_) {
            cb(event);
        }
    }
};

// A functor
struct FileLogger {
    void operator()(const std::string& msg) const {
        std::cout << "[File] " << msg << '\n';
    }
};

void console_log(const std::string& msg) {
    std::cout << "[Console] " << msg << '\n';
}

int main() {
    EventEmitter emitter;

    // Register different callable types — all stored as std::function
    emitter.on(console_log);             // function pointer
    emitter.on(FileLogger{});            // functor
    emitter.on([](const std::string& m) { // lambda
        std::cout << "[Lambda] " << m << '\n';
    });

    emitter.emit("user_login");
    return 0;
}
Buena práctica

Principio de Segregacion de Interfaces (ISP): prefiere interfaces pequenas y enfocadas sobre interfaces grandes y monoliticas. Una clase no debe ser forzada a implementar metodos que no usa. Divide IAnimal en IFlyable e ISwimmable si no todos los animales vuelan y nadan.

Principio de Inversion de Dependencias (DIP): los modulos de alto nivel deben depender de abstracciones (interfaces), no de implementaciones concretas. Pasa IRepository& en lugar de MySQLDatabase& — esto te permite intercambiar implementaciones para pruebas (repositorios simulados) y diferentes entornos.

Error común

Las jerarquias de herencia profundas (4+ niveles) se vuelven fragiles y dificiles de razonar. Un cambio en una clase base puede tener efectos inesperados en descendientes distantes. El problema de la clase base fragil convierte el mantenimiento en una pesadilla. Prefiere jerarquias planas (1-2 niveles), usa interfaces para el polimorfismo y compone comportamientos con objetos miembro o std::variant. Si la profundidad de tu jerarquia excede 3 niveles, es casi seguramente un olor de diseno.

Puntos clave
  • La composicion es mas flexible que la herencia — usala para reutilizacion de codigo (relaciones tiene-un)
  • Las interfaces abstractas puras definen contratos y habilitan el Principio de Inversion de Dependencias
  • std::variant con std::visit proporciona polimorfismo en tiempo de compilacion para conjuntos cerrados de tipos
  • std::function proporciona eliminacion de tipo — almacena cualquier callable con una firma compatible
  • Manten las jerarquias de herencia planas (1-2 niveles) y prefiere interfaces sobre clases base concretas
  • Aplica ISP (interfaces pequenas) y DIP (depende de abstracciones) para disenos mantenibles y testables

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Cuando deberias preferir la composicion sobre la herencia?

2. Que ventaja tiene `std::variant` sobre una jerarquia de herencia tradicional?

3. Que establece el Principio de Inversion de Dependencias (DIP)?