多重继承的陷阱:从C++到组合设计的演进

本文通过具体代码示例探讨C++多重继承的实际问题,比较了继承与组合两种设计方式的优劣,展示了如何通过组合解决多重继承带来的方法冲突和设计复杂性问题。

多重继承?

多年来,在Java出现后,当世界被C++主导,而我是一个真正的C++粉丝(是的,我承认),在2000年代左右的许多编程书籍/手册中,你可能会遇到这句话。

如果你的项目需要多重继承,那么使用C++。

当然,Java被创建时带有接口,以便能够实现真正重要的多重继承,其余情况实际上都是糟糕设计的产物。我当时并没有意识到这一点,但是…那个评论让我皱起了眉头。什么叫做只在需要多重继承的情况下使用?总是使用C++!

当然,我从中恢复了过来。我学会了欣赏Java,开始接触Python,几个月后…突然…C++对我来说变得陈旧而繁琐。如今,在我看来,它不仅仍然带有那种陈旧的味道(不必要的复杂性…),而且,随着时间的推移,那个关于C++的笑话:

C++是一只由狗改造而成的章鱼,被粘上了四条触手。

比以往任何时候都更加贴切。事实上,我认为很少有程序员能了解这门语言的所有细节(也许Raymond Chen和那些参与ISO C++标准工作的人除外)。

但好吧,让我们放下C++,专注于多重继承,这才是重点。

例如,我们可以有一个Triatleta(铁人三项运动员)类。铁人三项运动员游泳、骑自行车和跑步,所以第一个尝试可能是使用多重继承。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
#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;
}

…然后,一切都崩溃了。什么叫做get_gafas()调用不明确?我只是想显示眼镜的特性!让我们仔细看看CorredorNadadorCiclista类。实际上,CiclistaNadador有它们自己的GafasNadadorGafasCiclista对象。问题是方法名称相同:get_gafas()。我们能做什么?

好吧,这只是个小麻烦。毕竟,方法可以重命名。另一方面,C++允许限定方法所属的类。说做就做。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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;
}

…但这看起来像是个临时解决方案…

从设计的角度来看。这个解决方案正确吗?好吧,一个铁人三项运动员无疑是一个跑步者,一个游泳者,和一个骑自行车的人。从这个角度来看无可指责。但让我们继续完善。它符合Liskov替换原则吗?一个铁人三项运动员能否在没有变化且有意义的情况下,在与我们使用跑步者、游泳者或骑自行车者相同的情况下使用?实际上不能。想想眼镜!

最终,确实,这些出现的问题源于有问题的设计。实际上,除了继承多个没有属性的抽象类之外,永远不应该使用多重继承。也就是说,在Java中,这将被实现为接口。

那么,如果我们使用组合会怎样呢?让我们看看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
};

有些问题我们不能直接解决。现在没有get_gafas()方法,实际上,我们必须为每种类型创建一个适当的方法。也就是说,所选择的设计已经引导我们走向正确的道路,正如我们在下面看到的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
};

此外,现在代码是自文档化的:毫无疑问我们调用的是哪个运动员的哪个方法。并且调用get_gafas()时的歧义已经消失。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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;
}

最好尽可能远离多重继承。而且,最好远离继承本身。太多的代价。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计