Как спроектировать расширяемое множественное наследование? - PullRequest
0 голосов
/ 20 ноября 2018

У меня есть еще один вопрос о множественном наследовании, у которого есть ответ, например: здесь (но сфокусирован на отпечатке) или здесь (слишком расплывчато), но большинство ответов, на которые я наткнулся, этоподчеркивая недостатки производительности.Однако (как утверждает Бьярне Страуструп здесь ), это языковая особенность, которая должна быть предпочтительнее обходных путей.Вот более длинный пример, чтобы проиллюстрировать вопрос, следующий за примером:

Пример

В Чешской Республике номер рождения (эквивалентный SSN) назначается в следующем формате: YYMMDDXXX, поэтому давайтекласс для получения даты рождения в стандартном DMYYYY:

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 к своему месяцу.Итак, давайте унаследуем классы «Мужчина» и «Женщина», чтобы правильно получить дату:

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 году, через десять лет кто-нибудь может написать Фасад, чтобы правильно определить дату рождения для будущих тысячелетий:

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 года, когда мы испытаем возрождение 2000 года.Увы, также распространена практика, когда иммигрантам, родившимся до 1964 года, присваивается 10-значный номер рождения.

1 Ответ

0 голосов
/ 21 ноября 2018

Если вы не можете редактировать исходную библиотеку, вы можете попытаться решить ее с помощью «mixin», то есть новый класс фасадов параметризован своим собственным базовым классом Man или Woman.

Например:

#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 ]

В любом случае, вам лучше переопределить все эти классы, ИМХО лучший вариант - сохранить дату как целое числоили std :: chrono type (s) и пол в качестве поля перечисления.Предоставьте дополнительные фабричные методы для анализа строки формата даты и вставьте зависимости только в человеческий класс.

...