Автоматически вызовите метод базового класса C ++

StackOverflow https://stackoverflow.com/questions/3107974

  •  29-09-2019
  •  | 
  •  

Вопрос

Я пытаюсь реализовать Паттерн дизайна команды, но я спотыкаюсь о концептуальной проблеме. Допустим, у вас есть базовый класс и несколько подклассов, как в примере ниже:

class Command : public boost::noncopyable {
    virtual ResultType operator()()=0;

    //Restores the model state as it was before command's execution.
    virtual void undo()=0;

    //Registers this command on the command stack.
    void register();
};


class SomeCommand : public Command {
    virtual ResultType operator()(); // Implementation doesn't really matter here
    virtual void undo(); // Same
};

Дело в том, что каждый раз оператор () Вызывая в экземпляре SomeCommand, я хотел бы добавить *в стек (в основном для отмены целей), вызывая метод регистрации команды. Я хотел бы избежать вызова «регистрации» от somecommand :: operator () (), но чтобы его называли Automaticy (как-нибудь ;-))

Я знаю, что когда вы строите подклассный класс, такой как SomeCommand, конструктор базового класса называется Automaticy, поэтому я мог бы добавить вызов «Зарегистрироваться» там. То, что я не хочу звонить в регистр, пока не будет вызван Operator () ().

Как я могу это сделать? Я думаю, что мой дизайн несколько ошибочен, но я не знаю, как заставить эту работу.

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

Решение

Это выглядит так, как будто вы можете извлечь выгоду из идиомы NVI (не виртуального интерфейса). Там интерфейс command Объект не будет иметь виртуальных методов, но вызовут частные точки расширения:

class command {
public:
   void operator()() {
      do_command();
      add_to_undo_stack(this);
   }
   void undo();
private:
   virtual void do_command();
   virtual void do_undo();
};

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

Приложение: оригинал статья Херб Саттер, где он вводит концепцию (еще неназванная)

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

Разделите оператора двумя разными методами, например, выполнять и выполнять (если честно, мне не очень нравится оператор (). Сделайте команду :: execute не виртуально и команда :: executueImpl pure virtual, затем позвольте команду :: execute выполнить регистрацию, затем назовите его execututeimpl, например:

class Command
   {
   public:
      ResultType execute()
         {
         ... // do registration
         return executeImpl();
         }
   protected:
      virtual ResultType executeImpl() = 0;
   };

class SomeCommand
   {
   protected:
      virtual ResultType executeImpl();
   };

Предполагая, что это «нормальное» приложение с Undo и Redo, я бы не стал пытаться смешивать управление стеком с действиями, выполняемыми элементами в стеке. Это станет очень сложным, если у вас либо есть несколько цепочек отмены (например, более одной вкладки открытой), либо когда вы делаете, где команда должна знать, следует ли добавить себя в отмену или перемещаться от Redo к отмену, или перейти от Undo, чтобы переделать. Это также означает, что вам нужно издеваться над стеком Undo/Redo, чтобы проверить команды.

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

Вместо этого я бы создал класс onrionostack, который имеет функцию Execute_command (command*) и оставил команду как можно более простой.

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

Для получения подробной дискуссии о том, почему NVI желательны, см. Стандарты кодирования C ++ Herb Sutter. Там он заходит так далеко, чтобы предположить, что все публичные функции не виртуальны для достижения строгого отделения переопределяемого кода от кода публичного интерфейса (что не должно быть переоценено, чтобы вы всегда могли иметь некоторый централизованный контроль и добавлять инструменты, предварительно/пост- Проверка состояния и все, что вам нужно).

class Command 
{
public:
   void operator()() 
   {
      do_command();
      add_to_undo_stack(this);
   }

   void undo()
   {
      // This might seem pointless now to just call do_undo but 
      // it could become beneficial later if you want to do some
      // error-checking, for instance, without having to do it
      // in every single command subclass's undo implementation.
      do_undo();
   }

private:
   virtual void do_command() = 0;
   virtual void do_undo() = 0;
};

Если мы сделаем шаг назад и посмотрим на общую проблему, а не на немедленную задачу, я думаю, что Пит дает несколько очень хороших советов. Создание команды ответственной за добавление в стек Undo не является особенно гибким. Он может быть независимым от контейнера, в котором он находится. Эти обязанности на более высоком уровне, вероятно, должны быть частью фактического контейнера, который также можете возложить ответственность за выполнение и отмены команды.

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

Однажды у меня был проект по созданию приложения для 3D -моделирования, и для этого у меня было такое же требование. Насколько я понял, когда работал над этим, что независимо от того, что и операция всегда должна знать, что она сделала, и поэтому следует знать, как это отменить. Таким образом, у меня был базовый класс, созданный для каждой операции, и его состояние работы, как показано ниже.

class OperationState
{
protected:
    Operation& mParent;
    OperationState(Operation& parent);
public:
    virtual ~OperationState();
    Operation& getParent();
};

class Operation
{
private:
    const std::string mName;
public:
    Operation(const std::string& name);
    virtual ~Operation();

    const std::string& getName() const{return mName;}

    virtual OperationState* operator ()() = 0;

    virtual bool undo(OperationState* state) = 0;
    virtual bool redo(OperationState* state) = 0;
};

Создание функции, и его состояние было бы похоже на:

class MoveState : public OperationState
{
public:
    struct ObjectPos
    {
        Object* object;
        Vector3 prevPosition;
    };
    MoveState(MoveOperation& parent):OperationState(parent){}
    typedef std::list<ObjectPos> PrevPositions;
    PrevPositions prevPositions;
};

class MoveOperation : public Operation
{
public:
    MoveOperation():Operation("Move"){}
    ~MoveOperation();

    // Implement the function and return the previous
    // previous states of the objects this function
    // changed.
    virtual OperationState* operator ()();

    // Implement the undo function
    virtual bool undo(OperationState* state);
    // Implement the redo function
    virtual bool redo(OperationState* state);
};

Раньше был класс под названием OperationManager. Это зарегистрировало различные функции и создали их экземпляры в нем, как:

OperationManager& opMgr = OperationManager::GetInstance();
opMgr.register<MoveOperation>();

Функция регистра была похожа на:

template <typename T>
void OperationManager::register()
{
    T* op = new T();
    const std::string& op_name = op->getName();
    if(mOperations.count(op_name))
    {
        delete op;
    }else{
        mOperations[op_name] = op;
    }
}

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

void OperationManager::execute(const std::string& operation_name)
{
    if(mOperations.count(operation_name))
    {
        Operation& op = *mOperations[operation_name];
        OperationState* opState = op();
        if(opState)
        {
            mUndoStack.push(opState);
        }
    }
}

Когда есть необходимость отменять, вы делаете это от OperationManager, как:
OperationManager::GetInstance().undo();
И функция отмены операции Manager выглядит так:

void OperationManager::undo()
{
    if(!mUndoStack.empty())
    {
        OperationState* state = mUndoStack.pop();
        if(state->getParent().undo(state))
        {
            mRedoStack.push(state);
        }else{
            // Throw an exception or warn the user.
        }
    }
}

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

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