题
我正在尝试实施 命令设计模式, ,但是我绊倒了一个概念问题。假设您有一个基类和一些子类,例如以下示例:
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()()调用“寄存器”,但要称呼它为automaticaly(Someway ;-))
我知道,当您构建一个子类(例如SomeCommand)时,基类构造函数称为“自动”,因此我可以在其中添加一个呼叫。在调用操作员()()之前,我不想调用寄存器。
我怎样才能做到这一点?我想我的设计有些缺陷,但我真的不知道如何制作这项工作。
解决方案
看起来您可以从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介绍了这个概念(但未命名)
其他提示
将操作员分为两种不同的方法,例如执行和executeImpl(老实说,我真的不喜欢()操作员)。使命令::执行非虚拟和命令:: executeImpl pure virtual,然后让命令::执行执行注册,然后称其为executeImpl,如下:
class Command
{
public:
ResultType execute()
{
... // do registration
return executeImpl();
}
protected:
virtual ResultType executeImpl() = 0;
};
class SomeCommand
{
protected:
virtual ResultType executeImpl();
};
假设这是一个“普通”应用程序,并进行了撤消和重做,我不会尝试将堆栈管理堆栈的操作与堆栈上的元素执行的操作混合在一起。如果您要么有多个撤消链(例如,一个不止一个选项卡),或者当您执行undo-redo时,它会变得非常复杂或从撤消到重做。这也意味着您需要模拟撤消/重做堆栈以测试命令。
如果您确实想混合它们,那么您将有三个模板方法,每个模板方法都需要两个堆栈(或者需要在创建时对其操作的堆栈进行引用),并且每个堆栈都执行移动或添加,然后调用功能。但是,如果您确实有这三种方法,您会发现他们除了在命令上调用公共功能,并且没有由命令的任何其他部分使用,因此,下次您重新处理代码时,请成为候选人用于凝聚力。
相反,我将创建一个具有execute_command(命令*命令)函数的dunoredostack类,并尽可能简单地保留命令。
基本上,帕特里克的建议与大卫的建议相同,这也与我的建议一样。为此目的,请使用NVI(非虚拟接口习惯)。纯虚拟接口缺乏任何类型的集中控制。您也可以创建一个所有命令继承的单独的抽象基类,但是为什么要打扰呢?
有关NVI为何理想的详细讨论,请参见Herb Sutter的C ++编码标准。在那里,他甚至建议使所有公共功能都非虚拟词与公共接口代码的严格分离(不应该过分),以便您始终拥有一些集中式控制并添加仪器,并添加仪器,PRE/POST-条件检查以及您需要的其他任何内容)。
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;
};
如果我们退后一步,看一下一般问题,而不是提出的直接问题,我认为皮特提供了一些很好的建议。使命令负责将自己添加到撤消堆栈中并不是特别灵活。它可以独立于其居住的容器。这些高级职责可能应该是实际容器的一部分,您还可以使该命令负责执行和撤消命令。
然而,学习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;
}
}
每当要执行函数时,它将基于当前选择的对象或需要处理的任何内容。注意:就我而言,我不需要发送每个对象应移动多少的详细信息,因为一旦将其设置为活动函数,就可以通过移动设备从移动设备中计算出来。
在操作管理器中,执行函数就像:
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::GetInstance().undo();
而且,操作管理器的撤消功能看起来像这样:
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.
}
}
}
这使操作管理器不知道每个函数需要什么参数,因此很容易管理不同的功能。