¿Herencia múltiple?

Durante muchos años tras la aparición de Java, cuando el mundo estaba copado por C++, y yo era… un auténtico C++ fanboy (sí lo reconozco), te podías encontrar con esta frase en muchos libros/manuales de programación, alrededor de la década de los 2000.

Si necesitas herencia múltiple en tu proyecto, entonces usa C++.

Por supuesto, Java había sido creado con interfaces para poder hacer la herencia múltiple que realmente importaba, el resto de los casos siendo efectivamente producto de una mal diseño. Yo no era consciente de aquello, claro, pero… a mi aquel comentario me hacía fruncir el ceño. ¿Cómo que solo para casos en los que necesitaras herencia múltiple? ¡Utiliza C++ siempre!

Por supuesto, me curé de aquello. Aprendí a apreciar Java, comencé a meterme con Python, y unos meses después… de repente… C++ se me hacía viejo y farragoso. Hoy por hoy, en mi opinión, no solo sigue arrastrando ese sabor añejo (complejidad innecesaria…), sino que, con el tiempo, aquel chiste sobre C++:

C++ es un pulpo creado a partir de un perro al que se le han pegado cuatro tentáculos.

Está más candente que nunca. De hecho, me parece complicado que algún programador conozca todos los recovecos del lenguaje (quizás Raymond Chen, y aquellos que están trabajando en el estándar ISO C++).

Pero bueno, dejemos C++ y centrémonos en la herencia múltiple, que es lo importante.

Por ejemplo, podríamos tener una clase Triatleta. Los triatletas nadan, pedalean en una bicicleta y corren, así que una primera tentativa podría ser emplear herencia múltiple.

#include <iostream>


class Zapatillas {};
class Neopreno {};
class Bicicleta {};

class GafasCiclista {
public:
    std::string get_nombre() const
        { return "gafas de ciclista"; }
};

class GafasNadador {
public:
    std::string get_nombre() const
        { return "gafas de nadar"; }
};


class Corredor {
public:
    Corredor(const Zapatillas &z):
        zapatillas(z)
        {}

    void corre();
    void ata();
    const Zapatillas &get_equipacion() const
        { return zapatillas; }
private:
    Zapatillas zapatillas;
};


class Nadador {
public:
    Nadador(const Neopreno &n, const GafasNadador &g):
        neopreno(n), gafas(g)
        {}

    void nada();
    void ponte_traje();
    void quita_traje();
    const Neopreno &get_equipacion() const
        { return neopreno; }
    const GafasNadador &get_gafas() const
        { return gafas; }
private:
    Neopreno neopreno;
    GafasNadador gafas;
};


class Ciclista {
public:
    Ciclista(const Bicicleta &b, const GafasCiclista &g):
        bicicleta(b), gafas(g)
        {}

    void pedalea() const;
    void monta() const;
    const Bicicleta &get_equipacion() const
        { return bicicleta; }
    const GafasCiclista &get_gafas() const
        { return gafas; }
private:
    Bicicleta bicicleta;
    GafasCiclista gafas;
};


class Triatleta:
    public Corredor,
    public Nadador,
    public Ciclista
{
public:
    Triatleta(const Zapatillas &z,
              const Neopreno &n,
              const Bicicleta &b,
              const GafasNadador &gn,
              const GafasCiclista &gc):
          Corredor(z),
          Nadador(n, gn),
          Ciclista(b, gc)
    {
    }

    void compite()
    {
        ponte_traje();
        nada();
        quita_traje();
        monta();
        pedalea();
        ata();
        corre();
    }
};

int main() {
    GafasNadador gn;
    GafasCiclista gc;
    Neopreno n;
    Zapatillas z;
    Bicicleta b;
    Triatleta t(z, n, b, gn, gc);

    std::cout << t.get_gafas().get_nombre() << std::endl;
    return 0;
}

…y entonces, todo se derrumba. ¿Cómo que la llamada get_gafas()es ambigua? ¡Solo quiero mostrar las características de las gafas! Fijémonos bien en las clases Corredor, Nadador, y Ciclista. En realidad, Ciclista y Nadador tienen sus propios objetos GafasNadador y GafasCiclista. El problema es que los métodos se llaman igual: get_gafas(). ¿Qué se puede hacer?

Bueno, solo es una pequeña molestia. Al fin y al cabo, los métodos pueden renombrarse. Por otra parte, C++ permite cualificar la clase a la que pertenece el método a ejecutar. Dicho y hecho.

int main() {
    GafasNadador gn;
    GafasCiclista gc;
    Neopreno n;
    Zapatillas z;
    Bicicleta b;
    Triatleta t(z, n, b, gn, gc);

    std::cout << t.Nadador::get_gafas().get_nombre() << std::endl;
    std::cout << t.Ciclista::get_gafas().get_nombre() << std::endl;
    return 0;
}

…pero parece una chapucilla…

Desde un punto de vista de diseño. ¿La solución es correcta? Bueno, un triatleta sin duda es un corredor, es un nadador, y es un ciclista. Desde ese punto de vista no hay reproche. Pero sigamos refinando. ¿Se cumple el principio de sustitución de Liskov? Puede un triatleta emplearse sin cambios y con sentido, en las mismas situaciones en las que emplearíamos un corredor, un nadador o un ciclista? Lo cierto es que no. ¡Piensa en las gafas!

Al final, efectivamente, estos problemas que han surgido provienen de un diseño problemático. En realidad, nunca se debe utilizar herencia múltiple, más allá de la herencia de múltiples clases abstractas sin atributos. Es decir, lo que en Java se trataría de implementar interfaces.

¿Y qué pasa, en cambio, si utilizamos composición? Veamos.

class Triatleta {
public:
    Triatleta(const Zapatillas &z,
              const Neopreno &n,
              const Bicicleta &b,
              const GafasNadador &gn,
              const GafasCiclista &gc):
          corredor(z),
          nadador(n, gn),
          ciclista(b, gc)
    {}

    void compite()
    {
        nadador.ponte_traje();
        nadador.nada();
        nadador.quita_traje();
        ciclista.monta();
        ciclista.pedalea();
        corredor.ata();
        corredor.corre();
    }

private:
    Corredor corredor;
    Nadador nadador;
    Ciclista ciclista;
};

Hay cosas que no podemos solucionar directamente. Ahora no existe un método get_gafas(), y de hecho, debemos crear un método apropiado para cada tipo. Es decir, el diseño elegido ya nos lleva por el camino correcto, como podemos ver más abajo.

class Triatleta {
public:
    Triatleta(const Zapatillas &z,
              const Neopreno &n,
              const Bicicleta &b,
              const GafasNadador &gn,
              const GafasCiclista &gc):
          corredor(z),
          nadador(n, gn),
          ciclista(b, gc)
    {}

    void compite()
    {
        nadador.ponte_traje();
        nadador.nada();
        nadador.quita_traje();
        ciclista.monta();
        ciclista.pedalea();
        corredor.ata();
        corredor.corre();
    }

    const GafasNadador& get_gafas_nadador() const
        { return nadador.get_gafas(); }

    const GafasCiclista& get_gafas_ciclista() const
        { return ciclista.get_gafas(); }

private:
    Corredor corredor;
    Nadador nadador;
    Ciclista ciclista;
};

Además, ahora el código es autodocumentado: no hay duda de a qué métodos de cuál deportista estamos llamando. Y la ambigüedad al llamar a get_gafas() ha desaparecido.

int main() {
    GafasNadador gn;
    GafasCiclista gc;
    Neopreno n;
    Zapatillas z;
    Bicicleta b;
    Triatleta t(z, n, b, gn, gc);

    std::cout << t.get_gafas_nadador().get_nombre() << std::endl;
    std::cout << t.get_gafas_ciclista().get_nombre() << std::endl;
    return 0;
}

Es mejor mantenerse lo más alejado posible de la herencia múltiple. Es más, es mejor mantenerse alejado de la herencia en general. Demasiados impuestos.

Similar Posts