【问题标题】:How to design extensible multiple inheritance?如何设计可扩展的多重继承?
【发布时间】: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 

问题

问题是HumanManWoman 可能是二进制库的形式,其中客户端代码无法将继承重写为虚拟。那么如何正确设计可扩展库来实现多重继承呢?我应该将库范围内的每个继承都设为虚拟(因为我事先不知道如何扩展它),还是有更优雅的通用设计?

关于性能:这不是低级编程和编译器优化的领域,设计观点不应该优先于高级编程吗?为什么编译器不像在 RVO 或 inline 调用决策中那样自动虚拟化继承?

例子背后的悲伤故事

1954 年,一些受技术启发的官僚决定以某种方式添加第十个密码,以便数字可以被 11 整除。后来天才发现有些数字不能以这种方式修改。所以他发出了一个例外,在这些情况下,最后一个数字将为零。那年晚些时候,发布了一项内部指令,不允许出现此类例外情况。但与此同时,发布了一些 1000 多个出生数字,这些数字不能被 11 整除,但仍然合法。不管这种混乱,一年中的世纪可以通过直到2054年的数字长度来推断,那时我们将经历千年虫的复兴。唉,还有一种常见的做法是,1964 年之前出生的移民被分配一个 10 位数的出生号码。

【问题讨论】:

  • 我从这个问题中唯一了解到的是,捷克共和国可能会从改进他们的 id 生成方案中受益。
  • 对不起,最后的例子很长而且相当不相关的悲伤故事(希望不会让人分心),但我阅读了很多答案,并没有发现任何关于设计的有用信息。
  • 我认为如果可以避免的话,通过继承“修复”错误可能不是应该设计的。这并不是继承的真正目的,当他们告诉我们更喜欢封装而不是继承时,我很确定这是我们应该避免的。
  • 我认为在这种情况下,继承菱形的存在表明继承不是正确的机制。我似乎记得 Stroustrup 曾警告不要在其他机制可能更简单地解决问题的情况下类的扩散。简而言之,方法可以包含if,类可以包含bool
  • 您希望对这些标识符进行什么样的操作?感觉就像一个类可以拥有最新版本,并且您可以拥有尝试转换为旧格式的方法,以使用旧 API,例如TryConvertTo1944Form()

标签: c++ multiple-inheritance extensibility


【解决方案1】:

如果您无法编辑原始库,您可以尝试通过“mixin”解决它,即新的外观类由它们自己的基类ManWoman 参数化。

例如:

#include <iostream>
#include <system_error>
#include <cstring>
#include <memory>
#include <type_traits>

class Human {
protected:
    char output[11];
    char input[10];

public:
    Human (const char* number) {
        memcpy(input, number, 10);
        if(!number[10])
            throw std::system_error( std::make_error_code( std::errc::invalid_argument ) );
    }

    static int twoCharsToNum(const char* str) {
        if(!isdigit(str[0]) || !isdigit(str[1]))
            throw std::system_error( std::make_error_code( std::errc::invalid_argument ) );
        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;
    }
};

class Man:public Human {
public:
    Man(const char* number):
        Human(number)
    {}
};

class Woman : public Human {
public:
    Woman(const char* number):
        Human(number)
    {}
    virtual int getMonth() override {
        int result = Human::twoCharsToNum(input+2)-50;
        if(result<0)
            throw std::system_error( std::make_error_code( std::errc::invalid_argument ) );
        if(result==0 || result>12)
            throw std::system_error( std::make_error_code( std::errc::invalid_argument ) );
        return result;
    }
};

template<class GenderType>
class Human_Century21:public GenderType {
public:

    explicit Human_Century21(const char* number):
        GenderType(number)
    {
        // or use std::enabled_if etc
        static_assert( std::is_base_of<Human,GenderType>::value, "Gender type must inherit Human" );
    }

    virtual int getYear() override {
        int year = Human::twoCharsToNum(this->input);
        if(year<54 && std::strlen(this->input) == 10 )
            year += 2000;
        else
            year += 1900;
        return year;
    }
};


int main ()
{
    auto man = std::make_shared< Human_Century21<Man> >(  "530101123"  );
    std::cout << "Man: [ year: " << man->getYear() << ", month:" << man->getMonth() << " ]" << std::endl;
    auto woman = std::make_shared< Human_Century21<Woman> >( "54510112345" );
    std::cout << "Woman: [ year: " << woman->getYear() << ", month:" << woman->getMonth() << " ]" << std::endl;
    return 0;
}

输出:

Man: [ year: 1953, month:1 ]
Woman: [ year: 1954, month:1 ]

Aniway,您最好重新设计所有这些类,恕我直言,最好的选择 - 将日期存储为整数或 std::chrono 类型,并将性别存储为枚举字段。提供额外的工厂方法来解析日期格式字符串,并将依赖项注入到人类类中。

【讨论】:

    猜你喜欢
    • 2020-04-11
    • 1970-01-01
    • 2013-02-26
    • 2014-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-18
    • 1970-01-01
    相关资源
    最近更新 更多