Алмазное наследование (C ++) - PullRequest
19 голосов
/ 18 декабря 2008

Я знаю, что наследование алмазов считается плохой практикой. Тем не менее, у меня есть 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 и инициализировать его (через новый) в соответствии с необходимым типом команды. Поэтому я хочу сделать следующий дизайн (в дополнение к вышесказанному):

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, не реализовывали один и тот же метод.

Спасибо.

Ответы [ 7 ]

19 голосов
/ 19 декабря 2008

Наследование - это второе по силе (больше связывание) отношение в 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 () public.

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

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

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

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

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

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

9 голосов
/ 18 декабря 2008

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

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

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

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

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

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

5 голосов
/ 18 декабря 2008

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

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

4 голосов
/ 19 декабря 2008

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

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

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

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

1 голос
/ 19 декабря 2008

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

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

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

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 голос
/ 18 декабря 2008

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

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

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

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

0 голосов
/ 18 декабря 2008

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

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

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

...