DEV Community

¿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++.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

...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;
}
Enter fullscreen mode Exit fullscreen mode

...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;
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
canro91 profile image
Cesar Aguirre

Tengo que confesar que nunca he sido fan de C/C++. En mi segundo año de universidad, empezamos con Java y fue un alivio saber que no tenia punteros, herencia multiple y la sintaxis un poco extraña de C/C++

Collapse
 
baltasarq profile image
Baltasar García Perez-Schofield

En su momento, yo me sentía orgulloso de conocer detalles de implementación, cuestiones que me acercaban al hardware de la máquina. Aunque sigo teniendo C en muy buena estima, cada vez C++ me parece más horrendo. Y, por si fuera poco, el desarrollo en lenguajes de programación ha ido más por lo que tú comentas: más alto nivel, menos oportunidades de meter la pata, y, además, buscando la mayor productividad, como con Python. Dicho de otra manera, quitando aplicaciones muy "de nicho" (como los juegos 3D), para mi C++ cada vez tiene menos sentido.