0. 问题
问:
现有一个名为“猩猩”的基类以及它的两个派生类“直立猩猩”和“智慧猩猩”,需要设计一个既会直立行走又很聪明的“猩猩”类。按照常理,这个类的实例应该既是一种“直立猩猩”又是一种“智慧猩猩”,可是我的工作环境禁止使用多重继承,那么我该如何设计这个类呢?
1. 多重派生与“死亡菱形”
如果使用提问者说的多重继承,即:“直立智慧猩猩” 同时派生自 “直立猩猩” 和 “智慧猩猩”,而后两者又已经各自派生自 “猩猩” ,这就会得到一个 C++ 当中的 “死亡菱形”:
这种派生结构会发现一个方法(C++中更多称为:成员函数)多份实现的问题。比如,假设基类“猩猩” 有一个 “吃” 的方法;“直立猩猩” 和 “智慧猩猩” 类都派生自它,于是各有一份 “吃” 的成员方法,这很好;但是,又来了一个 “直立智慧猩猩” 同时派生自前两者,现在,当需要调用“直立智慧猩猩”的“吃”方法时,如果采用普通的写法,编译器就无法决定,到底是使用来自 “直立猩” 的“吃”,还是来自 “智慧猩” 的 “吃”。
不仅成员函数如此,成员数据也是这样。假设基类 “猩猩” 里有一个叫“嘴”的成员数据,那么在“直立智慧猩猩”那里,就会暗中变成有两张嘴。
我们以 B 代表 “猩猩”,以 Da 代表 “直立猩”,以 Db 代表 “智慧猩”,以 Dab 代表 “直立智慧猩”,则有:
struct B
{
int a;
void eat() {};
};
struct Da : B
{};
struct Db : B
{};
struct Dab : Da, Db
{
void test()
{
std::cout << this->a << std::endl;
this->eat();
}
};
编译至 test() 时,至少会报两个错:一是分不清this->a,二是分不清this->eat() ,注意这里的“分不清”英文是:“… is ambiguous”。
第一种解决方法是明确指定使用哪个基类的成员,比如:
this->Da::eat(); // 通过 Da:: 明确指定后面的 eat 来自 基类 Da
假设,“直立猩”和“智慧猩”有不同的 “吃” 法,于是各自在基类的基础又实现了自己的一份“吃”方法,那么,“直立智慧猩”可以依据需要,有时候按“直立猩”的吃法,有时候则走“智慧猩”的吃法,那么,这个方法就有点意义;但无论如何,有两张嘴,这就不叫“派生”了,这得叫“基因突变”。所以这种方法,能解决编译上的语法问题,但在语义层面往往说不通。
另一种方法是,是使用“虚继承”,这不是我们这个回答要说的话题,我们只简单说一句:通常也不太推荐。
2. 使用“能力”组合
只要还没有错得太深,那么推荐的方法通常是:推翻原有的继承体系设计,改用组合来实现。其基本思想是: 把“直立”和“智慧”,视为一种“能力”,那么,“猩猩” 就只需要一个“猩猩”类型就可以了,因为什么能力都不拥有的,就是“普通猩”(原来的基类),而拥有 “直立”能力的,就是“直立猩”,拥有“智慧”能力的,就是“智慧猩”,同时拥有二者的,就是“直立智慧猩”。
假设你在设计一种“进化”的猩猩,那么基于能力组合的设计不仅是正确的,恐怕还是唯一正确的设计;因为只有如此设计,才能在程序中实现让一只“普通猩” (可能由于经常爱思考)而进化成“智慧猩”,再加上因为经常跑步而进一步进化为“直立智慧猩”;甚至,可能因为后来成天躺床上玩手机刷短视频,而又退化成“普通猩”……
当然,很多时候,我们并不需要如此强大的灵活性,但纵使如此,我们也应该使用这种能力组合方法,来定义具体的 “直立猩”、“智慧猩”、“直立智慧猩”类。
3. 完整代码
下面是完整代码。代码后有在线运行的链接。为了直观,我们使用了汉字做为类名。大多数新版的C++ 编译器都支持使用汉字做标志符了。当然,不推荐在实际工作代码中使用。
#include <iostream>
class 能力
{
public:
virtual ~能力() = default;
virtual std::string 取能() = 0;
};
class 直立能力 : public 能力
{
public:
std::string 取能() override
{
return "我的一小步,是人类进化的一大步";
}
};
class 智慧能力 : public 能力
{
public:
std::string 取能() override
{
return "香蕉诚可贵,自由价更高。若为进化故,二者皆可抛";
}
};
class 下半身超能力 : public 能力
{
public:
std::string 取能() override
{
return "嘿嘿嘿嘿嘿……";
}
};
//-------------------------------------------------------------
class 猩猩
{
public:
virtual ~猩猩() = default;
virtual void 行走()
{
std::cout << "爬、爬、爬" << std::endl;
}
virtual void 思考猩生()
{
std::cout << "猩生就是吃香蕉" << std::endl;
}
};
class 直立猩 : public 猩猩
{
public:
void 行走() override
{
std::cout << 直立.取能() << std::endl;
}
protected:
直立能力 直立;
};
class 智慧猩 : public 猩猩
{
public:
void 思考猩生() override
{
std::cout << 智慧.取能() << std::endl;
}
protected:
智慧能力 智慧;
};
class 直立智慧猩 : public 猩猩
{
public:
直立智慧猩() : 下半身(nullptr) {}
~直立智慧猩() override { delete 下半身; }
void 设定贤者(bool 是)
{
if(是)
{
delete 下半身;
下半身 = nullptr;
}
else if (!下半身)
{
下半身 = new 下半身超能力;
}
}
void 行走() override
{
std::cout << 进化代价(&直立, "走") << std::endl;
}
void 思考猩生() override
{
std::cout << 进化代价(&智慧, "思考猩生") << std::endl;
}
protected:
std::string 进化代价(能力* 原始能, std::string const& action)
{
if (!下半身)
{
return 原始能->取能();
}
// 下半身思考 :
return "我不想" + action
+ ",我只想" + 下半身->取能()
+ "\n\t你让我" + 下半身->取能()
+ ",我才想:" + 原始能->取能();
}
protected:
直立能力 直立;
智慧能力 智慧;
下半身超能力* 下半身;
};
void 秀(猩猩 *xx, char const* type)
{
std::cout << "我是一只" << type << "猩";
std::cout << "\n我的行走就是:";
xx->行走();
std::cout << "偶尔我也会思考猩生:";
xx->思考猩生();
std::cout << std::endl;
}
int main()
{
猩猩 xx;
秀(&xx, "普通");
直立猩 zlx;
秀(&zlx, "直立");
智慧猩 zhx;
秀(&zhx, "智慧");
直立智慧猩 zlzhx;
秀(&zlzhx, "直立智慧");
zlzhx.设定贤者(false);
秀(&zlzhx, "下半身超能力直立智慧");
}
运行输出:
我是一只普通猩 我的行走就是:爬、爬、爬 偶尔我也会思考猩生:猩生就是吃香蕉 我是一只直立猩 我的行走就是:我的一小步,是人类进化的一大步 偶尔我也会思考猩生:猩生就是吃香蕉 我是一只智慧猩 我的行走就是:爬、爬、爬 偶尔我也会思考猩生:香蕉诚可贵,自由价更高。若为进化故,二者皆可抛 我是一只直立智慧猩 我的行走就是:我的一小步,是人类进化的一大步 偶尔我也会思考猩生:香蕉诚可贵,自由价更高。若为进化故,二者皆可抛 我是一只下半身超能力直立智慧猩 我的行走就是:我不想走,我只想嘿嘿嘿嘿嘿…… 你让我嘿嘿嘿嘿嘿……,我才想:我的一小步,是人类进化的一大步 偶尔我也会思考猩生:我不想思考猩生,我只想嘿嘿嘿嘿嘿…… 你让我嘿嘿嘿嘿嘿……,我才想:香蕉诚可贵,自由价更高。若为进化故,二者皆可抛