Ромбовидное наследование (C++)

19
задан Potatoswatter 20 December 2013 в 03:28
поделиться

7 ответов

Наследование является вторым самым сильным (больше связи) отношения в C++, которому предшествуют только дружбой. Если можно перепроектировать в использование только состава код будет более слабо связан. Если Вы не можете, то необходимо рассмотреть, должны ли все классы действительно наследоваться основе. Действительно ли это происходит из-за реализации или просто интерфейса? Вы захотите использовать какой-либо элемент иерархии как основной элемент? Или просто листы в Вашей иерархии, которые являются реальным Действием? Если только листы являются действиями, и Вы добавляете поведение, можно рассмотреть основанный на политике дизайн для этого типа состава поведений.

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

я обеспечиваю абстрактный класс так, чтобы различные инстанцирования шаблона могли быть сохранены (через указатели) в контейнере или переданы функциям как аргументы и называться полиморфно.

class ActionDelayPolicy_NoWait;

class ActionBase // Only needed if you want to use polymorphically different actions
{
public:
    virtual ~Action() {}
    virtual void run() = 0;
};

template < typename Command, typename DelayPolicy = ActionDelayPolicy_NoWait >
class Action : public DelayPolicy, public Command
{
public:
   virtual run() {
      DelayPolicy::wait(); // inherit wait from DelayPolicy
      Command::execute();  // inherit command to execute
   }
};

// Real executed code can be written once (for each action to execute)
class CommandSalute
{
public:
   void execute() { std::cout << "Hi!" << std::endl; }
};

class CommandSmile
{
public:
   void execute() { std::cout << ":)" << std::endl; }
};

// And waiting behaviors can be defined separatedly:
class ActionDelayPolicy_NoWait
{
public:
   void wait() const {}
};

// Note that as Action inherits from the policy, the public methods (if required)
// will be publicly available at the place of instantiation
class ActionDelayPolicy_WaitSeconds
{
public:
   ActionDelayPolicy_WaitSeconds() : seconds_( 0 ) {}
   void wait() const { sleep( seconds_ ); }
   void wait_period( int seconds ) { seconds_ = seconds; }
   int wait_period() const { return seconds_; }
private:
   int seconds_;
};

// Polimorphically execute the action
void execute_action( Action& action )
{
   action.run();
}

// Now the usage:
int main()
{
   Action< CommandSalute > salute_now;
   execute_action( salute_now );

   Action< CommandSmile, ActionDelayPolicy_WaitSeconds > smile_later;
   smile_later.wait_period( 100 ); // Accessible from the wait policy through inheritance
   execute_action( smile_later );
}

использование наследования позволяет открытым методам от реализаций политики быть доступными посредством шаблонного инстанцирования. Это запрещает использование агрегирования для объединения политик, поскольку никакие новые функциональные участники не могли быть продвинуты в интерфейс класса. В примере шаблон зависит от подождавшей политики () методика, которая характерна для всех политик ожидания. Теперь ожидание какое-то время периоду требуется время установленного срока, которое установлено в течение периода () открытый метод.

В примере, политика NoWait является просто конкретным примером политики WaitSeconds с набором периода к 0. Это было намеренным для маркировки этого, интерфейс политики не должен быть тем же. Другая реализация политики ожидания могла ожидать в ряде миллисекунд, тактов системных часов, или до некоторого внешнего события, путем обеспечения класса, который регистрируется как обратный вызов для данного события.

, Если Вам не нужен полиморфизм, можно посещать из примера базовый урок и виртуальные методы в целом. В то время как это может казаться чрезмерно сложным для текущего примера, можно выбрать добавление других политик к соединению.

При добавлении новых ортогональных поведений подразумевал бы экспоненциальный рост в количестве классов, если простое наследование используется (с полиморфизмом), с этим подходом можно просто реализовать каждую другую часть отдельно и склеить его в шаблоне Action.

, Например, Вы могли сделать свое действие периодическим и добавить политику выхода, которые определяют, когда выйти из периодического цикла. Первые опции, которые приходят на ум, являются LoopPolicy_NRuns и LoopPolicy_TimeSpan, LoopPolicy_Until. Этот метод политики (выход () в моем случае) называют однажды для каждого цикла. Первая реализация считает количество раз, это назвали выходами после постоянного числа (зафиксированный пользователем, поскольку период был зафиксирован в примере выше). Вторая реализация периодически выполняла бы процесс в течение данного периода времени, в то время как последний выполнит этот процесс до данного времени (часы).

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

Теперь я должен пойти, но если Вам интересно, я могу попытаться отправить измененную версию.

19
ответ дан 30 November 2019 в 03:29
поделиться

Существует различие качества дизайна между ориентированным на реализацию ромбовидным наследованием, где реализация наследована (опасное), и ориентированное на выделение подтипов наследование, где интерфейсы или интерфейсы маркера наследованы (часто полезный).

Обычно, если можно избежать первого, Вы более обеспечены с тех пор где-нибудь по линии, точный вызываемый метод может вызвать проблемы, и важность виртуальных оснований, состояний, и т.д., начинает иметь значение. На самом деле Java не позволил бы Вам вытягивать что-то как этот, он поддерживает только интерфейсную иерархию.

я думаю, что "самый чистый" дизайн, можно предстать перед этим, должен эффективно повернуть все классы в ромбе в ложные интерфейсы (при наличии никакой информации состояния и наличии чистых виртуальных методов). Это уменьшает влияние неоднозначности. И конечно, можно использовать несколько и даже ромбовидное наследование для этого точно так же, как Вы использовали бы реализации в Java.

Затем имейте ряд конкретных реализаций этих интерфейсов, которые могут быть реализованы по-разному (Например, агрегирование, даже наследование).

Инкапсулируют эту платформу так, чтобы внешние клиенты только получили интерфейсы и никогда не взаимодействовали непосредственно с конкретными типами и удостоверились, что полностью протестировали Ваши реализации.

, Конечно, это - большая работа, но если Вы пишете центральный и допускающий повторное использование API, это могло бы быть Вашим лучшим выбором.

9
ответ дан 30 November 2019 в 03:29
поделиться

Я столкнулся с этой проблемой только на этой неделе и нашел статью о DDJ, который объяснил проблемы и когда Вы должны или не должны быть обеспокоены ими.Вот:

"Множественное наследование, Продуманное Полезный"

5
ответ дан 30 November 2019 в 03:29
поделиться

"Ромбы" в иерархии наследования интерфейсов довольно безопасны - это - наследование кода что get's Вы в горячую воду.

Для получения повторного использования кода я советую Вам рассматривать mixins (Google для C++ Mixins, если Вы незнакомы с tequnique). При использовании mixins Вы чувствуете, что можно "пойти по магазинам" для фрагментов кода, что необходимо реализовать Вас класс, не используя множественное наследование классов с сохранением информации.

Так, шаблон - множественное наследование интерфейсов и единственная цепочка mixins (предоставление Вам повторное использование кода), чтобы помочь реализовать реальный класс.

Hope, которая помогает!

4
ответ дан 30 November 2019 в 03:29
поделиться

С первым примером.....

, должен ли ActionRead ActionWrite быть подклассами действия вообще.

, так как Вы собираетесь закончить с одним реальным классом, который будет действием так или иначе, Вы могли просто наследовать actionread и actionwrite без них являющийся действиями в себе.

, хотя, Вы могли изобрести код, который потребует, чтобы они были действиями. Но в целом я попытался бы разделить Действие, Чтение, Запись и Задержку, и просто реальный класс смешивает все это вместе

1
ответ дан 30 November 2019 в 03:29
поделиться

Со знанием больше того, что Вы делаете, я, вероятно, реорганизовал бы вещи немного. Вместо нескольких наследование со всеми этими версиями действия я сделал бы полиморфное чтение и запись и запись классов, instanciated как делегаты.

Что-то как следующее (который не имеет никакого ромбовидного наследования):

Здесь я представляю один из многих способов реализовать дополнительную Задержку и предполагаю, что методология задержки является тем же для всех читателей. каждый подкласс мог бы иметь их собственную реализацию задержки, в этом случае, Вы передадите Read и экземпляру соответствующего полученного класса Задержки.

class Action // abstract
{
   // Reader and writer would be abstract classes (if not interfaces)
   // from which you would derive to implement the specific
   // read and write protocols.

   class Reader // abstract
   {
      Class Delay {...};
      Delay *optional_delay; // NULL when no delay
      Reader (bool with_delay)
      : optional_delay(with_delay ? new Delay() : NULL)
      {};
      ....
   };

   class Writer {... }; // abstract

   Reader  *reader; // may be NULL if not a reader
   Writer  *writer; // may be NULL if not a writer

   Action (Reader *_reader, Writer *_writer)
   : reader(_reader)
   , writer(_writer)
   {};

   void read()
   { if (reader) reader->read(); }
   void write()
   { if (writer)  writer->write(); }
};


Class Flow : public Action
{
   // Here you would likely have enhanced version
   // of read and write specific that implements Flow behaviour
   // That would be comment to FlowA and FlowB
   class Reader : public Action::Reader {...}
   class Writer : public Action::Writer {...}
   // for Reader and W
   Flow (Reader *_reader, Writer *_writer)
   : Action(_reader,_writer)
   , writer(_writer)
   {};
};

class FlowA :public Flow  // concrete
{
    class Reader : public Flow::Reader {...} // concrete
    // The full implementation for reading A flows
    // Apparently flow A has no write ability
    FlowA(bool with_delay)
    : Flow (new FlowA::Reader(with_delay),NULL) // NULL indicates is not a writer
    {};
};

class FlowB : public Flow // concrete
{
    class Reader : public Flow::Reader {...} // concrete
    // The full implementation for reading B flows
    // Apparently flow B has no write ability
    FlowB(bool with_delay)
    : Flow (new FlowB::Reader(with_delay),NULL) // NULL indicates is not a writer
    {};
};
1
ответ дан 30 November 2019 в 03:29
поделиться

Для случая 2, OneCommand не просто особый случай CompositeCommand? Если Вы устраняете OneCommand и позволяете CompositeCommand с только иметь один элемент, я думаю, что Ваш дизайн становится более простым:

              CommandAbstraction
                 /          \
                /            \
               /              \
        ModernCommand      CompositeCommand
               \               /
                \             /
                 \           /
             ModernCompositeCommand

у Вас все еще есть страшный ромб, но я думаю, что это может быть приемлемым случаем для него.

-1
ответ дан 30 November 2019 в 03:29
поделиться
Другие вопросы по тегам:

Похожие вопросы: