Cargando...

Clases abstractas e interfaces en C++

Aprende clases abstractas e interfaces en C++ usando funciones virtuales puras para crear diseños flexibles.

En C++, las clases abstractas son aquellas que contienen al menos una función virtual pura y no pueden ser instanciadas directamente. El lenguaje C++ no tiene una palabra clave separada para “interfaz”; en su lugar, una clase abstracta compuesta solo por funciones virtuales puras actúa como una interfaz. En este artículo, exploraremos el diseño de clases abstractas e interfaces, la implementación de múltiples interfaces, el Principio de Inversión de Dependencias (DIP) y un ejemplo práctico con la clase Book.


1) Función virtual pura y clase abstracta

Una función virtual marcada con = 0 es una función virtual pura. Una clase con al menos una de estas funciones se convierte en una clase abstracta y no puede instanciarse directamente.


struct IPrintable {
    virtual ~IPrintable() = default;    // Buenas prácticas: destructor virtual en interfaces
    virtual void Print() const = 0;     // virtual pura → contrato de interfaz
};

struct IEntity {
    virtual ~IEntity() = default;
    virtual std::string Key() const = 0;
};

Estas dos clases ahora actúan como “interfaces”. Las clases que las implementen deben definir Print() y Key().


2) Clase base Book: clase abstracta

Expresemos el contrato común mediante una clase abstracta BookBase. Las clases derivadas definirán el cálculo del precio.


#include <iostream>
#include <memory>
#include <string>
using namespace std;

struct BookBase : IPrintable, IEntity {
protected:
    string title, author;
public:
    BookBase(string t, string a) : title(move(t)), author(move(a)) {}
    virtual ~BookBase() = default;

    // Virtual pura: las clases derivadas calculan el precio
    virtual double FinalPrice() const = 0;

    // Contrato de IPrintable
    void Print() const override {
        cout << title << " - " << author
             << " | " << FinalPrice() << " €" << endl;
    }

    // Contrato de IEntity
    string Key() const override { return title + ":" + author; }
};

BookBase no puede instanciarse directamente con new porque FinalPrice() es una función virtual pura. Sin embargo, podemos proporcionar implementaciones predeterminadas para Print() y Key().


3) Clases que implementan la interfaz (EBook / PrintedBook)

Creamos dos tipos diferentes de libros que cumplen con el contrato FinalPrice().


struct EBook : BookBase {
    double base, vat;
    EBook(string t, string a, double b, double v)
        : BookBase(move(t), move(a)), base(b), vat(v) {}

    double FinalPrice() const override { return base * (1.0 + vat); }
};

enum class Cover { Soft, Hard };

struct PrintedBook : BookBase {
    double base, shipping; int pages; Cover cover;
    PrintedBook(string t, string a, double b, double s, int p, Cover c)
        : BookBase(move(t), move(a)), base(b), shipping(s), pages(p), cover(c) {}

    double FinalPrice() const override {
        double cost = base + shipping + 0.04 * pages;
        if (cover == Cover::Hard) cost *= 1.1;
        return cost;
    }
};

Ambas clases cumplen con el contrato BookBase, por lo que pueden usarse de forma polimórfica.


4) Implementación de múltiples interfaces (Segregación de interfaces)

Diseñar interfaces pequeñas y enfocadas en lugar de una grande reduce los costos de mantenimiento. Por ejemplo, una interfaz IDiscountable puede definir la política de descuentos.


struct IDiscountable {
    virtual ~IDiscountable() = default;
    virtual double ApplyDiscount(double rate) const = 0; // 0.10 → 10 %
};

struct DiscountedEBook : EBook, IDiscountable {
    using EBook::EBook; // hereda los constructores
    double ApplyDiscount(double rate) const override {
        return FinalPrice() * (1.0 - rate);
    }
};

Con este diseño, DiscountedEBook implementa tanto el contrato de BookBase como el de descuento.


5) Inversión de dependencias (DIP) y capacidad de prueba

Las clases deben depender de abstracciones (interfaces) y no de tipos concretos. Esto permite un acoplamiento débil y facilita las pruebas unitarias. A continuación, un “motor de precios” utiliza solo la interfaz BookBase.


struct PricingEngine {
    double Total(const vector<shared_ptr<BookBase>>& items) const {
        double sum = 0.0;
        for (const auto& b : items) sum += b->FinalPrice();
        return sum;
    }
};

int main() {
    vector<shared_ptr<BookBase>> items;
    items.push_back(make_shared<EBook>("Modern C++", "A. Dev", 100, 0.1));
    items.push_back(make_shared<PrintedBook>("Clean Code", "R. Martin", 120, 20, 464, Cover::Hard));

    PricingEngine eng;
    cout << "Total: " << eng.Total(items) << " €" << endl;

    for (const auto& b : items) b->Print(); // IPrintable
}

PricingEngine no depende de clases concretas; solo trabaja con la interfaz BookBase. Por tanto, puede usar un MockBook en las pruebas unitarias.


6) Interfaces con herencia múltiple

C++ admite la herencia múltiple; en el caso de las interfaces, esto suele ser seguro ya que no contienen estado. Sin embargo, usar herencia múltiple con clases con estado puede causar el problema del diamante. En tales casos, use herencia virtual o, preferiblemente, composición.


struct IStorable  { virtual ~IStorable()=default;  virtual void Save() const =0; };
struct ISearchable{ virtual ~ISearchable()=default; virtual bool Matches(string) const =0; };

struct SearchableBook : BookBase, IStorable, ISearchable {
    using BookBase::BookBase;

    double FinalPrice() const override { return 0.0; } // ejemplo simple
    void Save() const override { /* guardar en archivo... */ }
    bool Matches(string q) const override {
        return title.find(q)!=string::npos || author.find(q)!=string::npos;
    }
};

7) Ejemplo de fábrica (Factory) con interfaz

Para separar la lógica de creación de las clases concretas, podemos usar una interfaz de fábrica.


struct IBookFactory {
    virtual ~IBookFactory() = default;
    virtual shared_ptr<BookBase> MakeEBook(string t,string a,double b,double v)=0;
    virtual shared_ptr<BookBase> MakePrinted(string t,string a,double b,double s,int p,Cover c)=0;
};

struct DefaultBookFactory : IBookFactory {
    shared_ptr<BookBase> MakeEBook(string t,string a,double b,double v) override {
        return make_shared<EBook>(move(t), move(a), b, v);
    }
    shared_ptr<BookBase> MakePrinted(string t,string a,double b,double s,int p,Cover c) override {
        return make_shared<PrintedBook>(move(t), move(a), b, s, p, c);
    }
};

Los módulos de alto nivel dependen solo de IBookFactory; la implementación concreta puede reemplazarse fácilmente o simularse en pruebas.


8) Consejos de diseño


9) Errores comunes


10) TL;DR

  • En C++, una “interfaz” = una clase abstracta compuesta solo por funciones virtuales puras.
  • Las clases abstractas no pueden instanciarse; las clases derivadas se concretan al sobrescribir el contrato.
  • Prefiera interfaces pequeñas y coherentes (ISP) y dependa de abstracciones (DIP).
  • Use punteros o referencias para el polimorfismo; no olvide el destructor virtual.
  • Use herencia múltiple para interfaces; para compartir estado, prefiera la composición.
  • Todos los ejemplos funcionan en Visual Studio 2022 y GCC 11+.

Artículos relacionados