Алмазное наследование (C ++)

Я знаю, что алмазное наследование считается плохой практикой. Однако у меня есть 2 случая, в которых, как мне кажется, алмазное наследование может очень хорошо вписаться. Я хочу спросить, порекомендуете ли вы мне использовать алмазное наследование в этих случаях, или есть другой дизайн, который мог бы быть лучше.

Случай 1: я хочу создать классы, которые представляют различные виды «Действия» в моей системе. Действия классифицируются по нескольким параметрам:

  • Действие может быть «Чтение» или «Запись».
  • Действие может быть с задержкой или без задержки (это не просто параметр 1. Это значительно меняет поведение).
  • «Тип потока» действия может быть FlowA или FlowB.

Я намерен получить следующий дизайн:

// abstract classes
class Action  
{
    // methods relevant for all actions
};
class ActionRead      : public virtual Action  
{
    // methods related to reading
};
class ActionWrite     : public virtual Action  
{
    // methods related to writing
};
class ActionWithDelay : public virtual Action  
{
    // methods related to delay definition and handling
};
class ActionNoDelay   : public virtual Action  {/*...*/};
class ActionFlowA     : public virtual Action  {/*...*/};
class ActionFlowB     : public virtual Action  {/*...*/};

// concrete classes
class ActionFlowAReadWithDelay  : public ActionFlowA, public ActionRead, public ActionWithDelay  
{
    // implementation of the full flow of a read command with delay that does Flow A.
};
class ActionFlowBReadWithDelay  : public ActionFlowB, public ActionRead, public ActionWithDelay  {/*...*/};
//...

Конечно, я буду подчиняться, что никакие 2 действия (унаследованные от класса Action) не будут реализовывать один и тот же метод.

Случай 2: я использую составной шаблон проектирования для «Команды» в своей системе. Команду можно читать, записывать, удалять и т. Д. Я также хочу иметь последовательность команд, которая также может быть прочитана, записана, удалена и т. Д. Последовательность команд может содержать другие последовательности команд.

Итак, у меня такой дизайн:

class CommandAbstraction
{
    CommandAbstraction(){};
    ~CommandAbstraction()=0;
    void Read()=0;
    void Write()=0;
    void Restore()=0;
    bool IsWritten() {/*implemented*/};
    // and other implemented functions
};

class OneCommand : public virtual CommandAbstraction
{
    // implement Read, Write, Restore
};

class CompositeCommand : public virtual CommandAbstraction
{
    // implement Read, Write, Restore
};

Кроме того, у меня есть особый вид команд, «современные» команды. И одна команда, и составная команда могут быть современными. Быть «современным» добавляет определенный список свойств к одной команде и составной команде (в основном одинаковые свойства для них обоих). Я хочу иметь возможность удерживать указатель на CommandAbstraction и инициализировать его (через new) в соответствии с необходимым типом команды. Итак, я хочу сделать следующий дизайн (в дополнение к вышеупомянутому):

class ModernCommand : public virtual CommandAbstraction
{
    ~ModernCommand()=0;
    void SetModernPropertyA(){/*...*/}
    void ExecModernSomething(){/*...*/}
    void ModernSomethingElse()=0;

};
class OneModernCommand : public OneCommand, public ModernCommand
{
    void ModernSomethingElse() {/*...*/};
    // ... few methods specific for OneModernCommand
};
class CompositeModernCommand : public CompositeCommand, public ModernCommand
{
    void ModernSomethingElse() {/*...*/};
    // ... few methods specific for CompositeModernCommand
};

Опять же, я буду уверен, что никакие 2 класса, унаследованные от класса CommandAbstraction, не будут реализовывать один и тот же метод.

Спасибо.


person Igor Oks    schedule 18.12.2008    source источник


Ответы (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 );
}

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

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

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

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

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

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

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

person David Rodríguez - dribeas    schedule 18.12.2008
comment
Привет Дэвид. Я заинтересован в изучении продвинутых тем ООП (наследования), как вы упомянули в своих сообщениях выше (функторы, виртуальные функции и т. Д.). Можете ли вы порекомендовать какие-либо книги по этой теме? - person ; 19.10.2010

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

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

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

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

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

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

person Uri    schedule 18.12.2008

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

«Множественное наследование считается полезным»

person Nathan Fellman    schedule 18.12.2008

«Бубны» в иерархии наследования интерфейсов вполне безопасны - это наследование кода, которое заводит вас в горячую воду.

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

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

Надеюсь, это поможет!

person Daniel Paull    schedule 18.12.2008
comment
Соглашаться. Дизайн на основе политик и миксины довольно тесно связаны: для первого требуется предыдущий дизайн для тех случаев, когда добавленный код изменяет поведение, в то время как последний можно просто добавить в конце, но он позволяет только добавлять поведение (но не изменять). Проблема диктует, достаточно ли этого. - person David Rodríguez - dribeas; 19.12.2008
comment
Я думаю, это хорошо сказано - хорошо знать разные шаблоны и их применимость к различным ситуациям. Я также думаю, что стоит отметить, что как с примесями, так и с подходами с политикой / характеристикой компилятор может оптимизировать способы, которые он не может, если код похоронен в базовых классах. - person Daniel Paull; 19.12.2008

В первом примере .....

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

так как вы собираетесь получить один конкретный класс, который в любом случае будет действием, вы можете просто унаследовать actionread и actionwrite, при этом они сами по себе не будут действиями.

хотя вы могли бы изобрести код, который потребовал бы, чтобы они были действиями. Но в целом я бы попытался разделить Action, Read, Write и Delay, и просто конкретный класс смешивает все это вместе.

person Keith Nicholas    schedule 18.12.2008

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

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

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

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
    {};
};
person Roger Nelson    schedule 18.12.2008

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

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

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

person Michael Kristofik    schedule 18.12.2008
comment
Спасибо, но нет, OneCommand и CompositeCommand принципиально разные. - person Igor Oks; 19.12.2008