Подтвердить что ты не робот

Наследование диаманта (С++)

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

Случай 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: Я реализую составной шаблон проектирования для "Command" в моей системе. Команда может быть прочитана, записана, удалена и т.д. Я также хочу иметь последовательность команд, которые также могут быть прочитаны, записаны, удалены и т.д. Последовательность команд может содержать другие последовательности команд.

Итак, у меня есть следующий дизайн:

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
};

Кроме того, у меня есть особый вид команд "Modern". Обе команды и составная команда могут быть современными. Будучи "Современным", добавляется определенный список свойств для одной команды и составной команды (в основном одинаковые свойства для них обоих). Я хочу иметь возможность удерживать указатель на 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, будут реализовывать один и тот же метод.

Спасибо.

4b9b3361

Ответ 1

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

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

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

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(), который является общим для всех политик ожидания. Теперь ожидание периода времени требует фиксированного периода времени, заданного через public() публичный метод period.)

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

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

Ответ 4

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

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

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

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

Ответ 5

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

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

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