【发布时间】:2018-11-20 20:07:02
【问题描述】:
我还有一个关于多重继承设计的问题,它有一个答案,即here(但专注于足迹)或here(太模糊),但我偶然发现的大多数答案都是强调性能缺陷。但是(正如 Bjarne Stroustrup 声称的 here),它是一种语言功能,应该优先于解决方法。这是一个更长的示例来说明示例后面的问题:
示例
在捷克共和国,出生号码(相当于 SSN)按以下格式分配: YYMMDDXXX,所以让我们有一个类来获取标准 D.M.YYYY 中的出生日期:
class Human {
protected:
char output[11];
char input[10];
public:
Human (const char* number) {
strncpy(input, number, 10);
if(!number[10]) throw E_INVALID_NUMBER;
}
static int twoCharsToNum(const char* str) {
if(!isdigit(str[0]) || !isdigit(str[1])) throw E_INVALID_NUMBER;
return (str[0]-'0')*10 +str[1]-'0';
}
const char* getDate() {
sprintf(output, "%d.%d.%d", getDay(), getMonth(), getYear());
return output;
}
// range check omitted here to make code short
virtual int getDay() { return twoCharsToNum(input+4); }
virtual int getMonth() { return twoCharsToNum(input+2); }
virtual int getYear() { return twoCharsToNum(input)+1900; }
};
三种方法是虚拟的,因为女性每月获得 +50。所以让我们继承 Man 和 Woman 类来正确获取日期:
class Man : public Human {
public:
using Human::Human;
};
class Woman : public Human {
public:
using Human::Human;
int getMonth() {
int result = twoCharsToNum(input+2)-50;
if(result<0) throw E_INVALID_GENDER;
if(result==0 || result>12) throw E_INVALID_RANGE;
return result;
}
};
自 1954 年以来,该数字有 4 位数的附录,而不是 3 位(在这个问题的末尾提到了一个悲伤的故事)。如果该库是在 1944 年编写的,那么十年后有人可以编写一个 Facade 来为未来的千禧一代正确获取出生日期:
class Human2 : public Human {
public:
using Human::Human;
virtual int getYear() {
int year = twoCharsToNum(input);
if(year<54 && strlen(number)==10) year+= 2000;
else year+= 1900;
return year;
}
};
class Man2 : public Human2 {
public:
using Human2::Human2;
};
在class Woman2中我们需要Woman::getMonth方法,所以我们需要解决菱形问题:
class Human2 : virtual public Human { ... };
class Woman : virtual public Human { ... }; // here is the real issue
class Woman2 : public Human2, public Woman {
using Human2::Human2;
using Woman::Woman;
};
钻石问题图:
Woman2
^ ^
| |
Woman Human2
^ ^
| |
Human
问题
问题是Human、Man 和Woman 可能是二进制库的形式,其中客户端代码无法将继承重写为虚拟。那么如何正确设计可扩展库来实现多重继承呢?我应该将库范围内的每个继承都设为虚拟(因为我事先不知道如何扩展它),还是有更优雅的通用设计?
关于性能:这不是低级编程和编译器优化的领域,设计观点不应该优先于高级编程吗?为什么编译器不像在 RVO 或 inline 调用决策中那样自动虚拟化继承?
例子背后的悲伤故事
1954 年,一些受技术启发的官僚决定以某种方式添加第十个密码,以便数字可以被 11 整除。后来天才发现有些数字不能以这种方式修改。所以他发出了一个例外,在这些情况下,最后一个数字将为零。那年晚些时候,发布了一项内部指令,不允许出现此类例外情况。但与此同时,发布了一些 1000 多个出生数字,这些数字不能被 11 整除,但仍然合法。不管这种混乱,一年中的世纪可以通过直到2054年的数字长度来推断,那时我们将经历千年虫的复兴。唉,还有一种常见的做法是,1964 年之前出生的移民被分配一个 10 位数的出生号码。
【问题讨论】:
-
我从这个问题中唯一了解到的是,捷克共和国可能会从改进他们的 id 生成方案中受益。
-
对不起,最后的例子很长而且相当不相关的悲伤故事(希望不会让人分心),但我阅读了很多答案,并没有发现任何关于设计的有用信息。
-
我认为如果可以避免的话,通过继承“修复”错误可能不是应该设计的。这并不是继承的真正目的,当他们告诉我们更喜欢封装而不是继承时,我很确定这是我们应该避免的。
-
我认为在这种情况下,继承菱形的存在表明继承不是正确的机制。我似乎记得 Stroustrup 曾警告不要在其他机制可能更简单地解决问题的情况下类的扩散。简而言之,方法可以包含
if,类可以包含bool。 -
您希望对这些标识符进行什么样的操作?感觉就像一个类可以拥有最新版本,并且您可以拥有尝试转换为旧格式的方法,以使用旧 API,例如TryConvertTo1944Form()
标签: c++ multiple-inheritance extensibility