Вызовите метод базового класса C++ автоматически
Я пытаюсь реализовать шаблон проектирования команд, но сталкиваюсь с концептуальной проблемой. Допустим, у вас есть базовый класс и несколько подклассов, как в примере ниже:
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, я хотел бы добавить * это в стек (в основном для отмены), вызвав метод регистра команды. Я хотел бы не вызывать "register" из SomeCommand::operator()(), но чтобы он вызывался автоматически (иногда;-))
Я знаю, что когда вы создаете подкласс, такой как SomeCommand, конструктор базового класса вызывается автоматически, поэтому я мог бы добавить туда вызов "register". То, что я не хочу вызывать, зарегистрировать, пока не будет вызван operator () ().
Как я могу это сделать? Я предполагаю, что мой дизайн несколько испорчен, но я действительно не знаю, как заставить это работать.
5 ответов
Похоже, что вы можете извлечь выгоду из идиомы NVI (Non-Virtual Interface). Там интерфейс 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 и executeImpl (если честно, мне не очень нравится оператор ()). Сделайте Command::execute не виртуальным, а Command::executeImpl чисто виртуальным, затем позвольте Command::execute выполнить регистрацию, а затем вызовите executeImpl, например:
class Command
{
public:
ResultType execute()
{
... // do registration
return executeImpl();
}
protected:
virtual ResultType executeImpl() = 0;
};
class SomeCommand
{
protected:
virtual ResultType executeImpl();
};
Предполагая, что это "нормальное" приложение с отменой и возвратом, я бы не стал смешивать управление стеком с действиями, выполняемыми элементами в стеке. Это будет очень сложно, если у вас либо несколько цепочек отмены (например, открыто более одной вкладки), либо когда вы делаете-отмену-повтор, где команда должна знать, добавлять ли себя, чтобы отменить, или перемещать себя из повторения в отмену, или переместить себя от отмены до повтора. Это также означает, что вам нужно смоделировать стек отмены / повтора для проверки команд.
Если вы хотите смешать их, то у вас будет три шаблонных метода, каждый из которых берет два стека (или объект команды должен иметь ссылки на стеки, с которыми он работает при создании), и каждый выполняет перемещение или добавление, затем вызывая функция. Но если у вас есть эти три метода, вы увидите, что на самом деле они не делают ничего, кроме вызова публичных функций в команде, и не используются какой-либо другой частью команды, поэтому станьте кандидатами в следующий раз, когда вы выполните рефакторинг своего кода. для сплоченности.
Вместо этого я бы создал класс UndoRedoStack, который имеет функцию execute_command(Command*command), и оставил команду как можно более простой.
Когда-то у меня был проект по созданию приложения для 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;
}
}
Всякий раз, когда функция должна была быть выполнена, она основывалась бы на выбранных в данный момент объектах или на том, с чем ей нужно работать. ПРИМЕЧАНИЕ. В моем случае мне не нужно было отправлять детали того, сколько должен перемещаться каждый объект, потому что это вычислялось MoveOperation с устройства ввода, как только оно было установлено в качестве активной функции.
В OperationManager выполнение функции будет выглядеть так:
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();
И функция отмены OperationManager выглядит так:
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.
}
}
}
Это заставляло OperationManager не знать, какие аргументы нужны каждой функции, и поэтому было легко управлять различными функциями.
По сути, предложение Патрика совпадает с предложением Дэвида, которое также совпадает с моим. Для этой цели используйте 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;
};
Если мы сделаем шаг назад и посмотрим на общую проблему вместо того, чтобы сразу же задавать вопрос, я думаю, что Пит даст несколько очень хороших советов. Назначение Command ответственным за добавление себя в стек отмены не особенно гибко. Он может быть независимым от контейнера, в котором он находится. Эти высокоуровневые обязанности, вероятно, должны быть частью реального контейнера, который вы также можете сделать ответственным за выполнение и отмену команды.
Тем не менее, это должно быть очень полезно для изучения NVI. Я видел, что слишком много разработчиков пишут чисто виртуальные интерфейсы, подобные этому, из исторических преимуществ, которые они имели, только добавляя один и тот же код в каждый подкласс, который его определяет, когда его нужно реализовывать только в одном центральном месте. Это очень удобный инструмент для добавления в ваш инструментарий программирования.