我正在尝试实施command design pattern,但我遇到了一个概念问题。假设您有一个基类和一些子类,如下例所示:
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实例上调用operator ()
时,我都想通过调用Command的register方法将* this添加到堆栈中(主要用于撤消)。我想避免从SomeCommand :: operator()()中调用“register”,但要将其称为automaticaly(someway ;-))
我知道当你构造一个像SomeCommand这样的子类时,基类构造函数被称为automaticaly,所以我可以在那里添加一个“register”调用。在调用operator()()之前,我不想调用它。
我该怎么做?我想我的设计有些缺陷,但我真的不知道如何使这项工作。
答案 0 :(得分:27)
看起来您可以从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,您将获得更多更好的解释。
附录:Herb Sutter的原始article,他介绍了这个概念(尚未命名)
答案 1 :(得分:6)
使用两种不同的方法拆分运算符,例如execute和executeImpl(说实话,我真的不喜欢()运算符)。 Make Command :: execute non-virtual,和Command :: executeImpl pure virtual,然后让Command :: execute执行注册,然后调用executeImpl,如下所示:
class Command
{
public:
ResultType execute()
{
... // do registration
return executeImpl();
}
protected:
virtual ResultType executeImpl() = 0;
};
class SomeCommand
{
protected:
virtual ResultType executeImpl();
};
答案 2 :(得分:1)
假设它是一个具有撤销和重做的“正常”应用程序,我不会尝试将堆栈管理与堆栈上的元素执行的操作混合在一起。如果您有多个撤销链(例如打开多个选项卡),或者执行-undore-redo,那么命令必须知道是将其自身添加到撤消还是从重做移动到撤消,这将变得非常复杂,或者从撤消到重做。它还意味着您需要模拟撤消/重做堆栈以测试命令。
如果你想混合它们,那么你将有三个模板方法,每个方法占用两个堆栈(或者命令对象需要引用它在创建时操作的堆栈),并且每个方法执行移动或添加,然后调用该函数。但是如果你确实有这三种方法,你会看到除了在命令上调用公共函数之外它们实际上没有做任何事情,并且命令的任何其他部分都没有使用它们,所以在下次重构代码时成为候选者凝聚力。
相反,我创建了一个具有execute_command(Command *命令)功能的UndoRedoStack类,并使命令尽可能简单。
答案 3 :(得分:0)
帕特里克的建议基本上与大卫的建议相同,也与我的相同。为此,请使用NVI(非虚拟接口惯用法)。纯虚拟接口缺乏任何集中控制。您也可以创建一个单独的抽象基类,所有命令都继承,但为什么要这么麻烦?
有关为什么需要NVI的详细讨论,请参阅Herb Sutter的C ++编码标准。在那里,他甚至建议将所有公共功能非虚拟化,以实现严格分离可覆盖代码与公共接口代码(不应该是可覆盖的,以便您可以始终拥有一些集中控制并添加仪器,前/后条件检查,以及你需要的任何其他东西)。
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;
};
如果我们退一步看看一般问题而不是直接询问的问题,我认为Pete提供了一些非常好的建议。使命令负责将自己添加到撤消堆栈并不是特别灵活。它可以独立于它所在的容器。那些更高级别的职责应该是实际容器的一部分,您也可以负责执行和撤消命令。
然而,研究NVI应该非常有帮助。我见过太多的开发人员编写像这样的纯虚拟接口,他们只需要将相同的代码添加到每个定义它的子类时,只需要在一个中心位置实现它。这是一个非常方便的工具,可以添加到您的编程工具箱中。
答案 4 :(得分:0)
我曾经有一个项目来创建一个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不知道每个函数需要什么参数,因此很容易管理不同的函数。