Вопрос

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

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

Спасибо.

Это было полезно?

Решение

Наследование - это вторые по силе (более связывающие) отношения в 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() в моем случае ) вызывается один раз для каждого цикла.Первая реализация подсчитывает, сколько раз она вызывалась exits после фиксированного числа (фиксированного пользователем, поскольку период был фиксирован в примере выше).Вторая реализация будет периодически запускать процесс в течение заданного периода времени, в то время как последняя будет запускать этот процесс до заданного времени (clock).

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

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

Другие советы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Здесь я представляю один из многих способов реализации необязательной задержки, и предположим, что методология задержки одинакова для всех читателей.каждый подкласс может иметь свою собственную реализацию delay в этом случае вы должны перейти к чтению и экземпляру соответствующего производного класса 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
    {};
};

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top