12

我正在尝试实现命令设计模式,但我遇到了一个概念问题。假设您有一个基类和一些子类,如下例所示:

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 实例上调用运算符时,我想通过调用 Command 的 register 方法将 *this 添加到堆栈(主要用于撤消目的)。我想避免从 SomeCommand::operator()() 调用“注册”,而是自动调用它(以某种方式 ;-))

我知道当你构造一个像 SomeCommand 这样的子类时,会自动调用基类构造函数,所以我可以在那里添加一个“注册”调用。在调用 operator()() 之前我不想调用 register 的东西。

我怎样才能做到这一点?我想我的设计有些缺陷,但我真的不知道如何完成这项工作。

4

5 回答 5

28

看起来您可以从 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 的原始文章,他介绍了这个概念(但未命名)

于 2010-06-24T07:39:59.253 回答
6

将运算符拆分为两种不同的方法,例如 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();
   };
于 2010-06-24T07:42:21.377 回答
1

假设它是一个带有撤消和重做的“普通”应用程序,我不会尝试将管理堆栈与堆栈上的元素执行的操作混合在一起。如果您有多个撤消链(例如打开多个选项卡),或者当您执行撤消重做时,命令必须知道是将自身添加到撤消还是将自身从重做移动到撤消,这将变得非常复杂,或将自身从撤消移动到重做。这也意味着您需要模拟撤消/重做堆栈来测试命令。

如果您确实想混合使用它们,那么您将拥有三个模板方法,每个方法采用两个堆栈(或者命令对象需要在创建时引用它所操作的堆栈),并且每个执行移动或添加,然后调用功能。但是如果你确实有这三种方法,你会发现它们实际上除了在命令上调用公共函数之外没有做任何事情,并且没有被命令的任何其他部分使用,所以下次重构代码时成为候选者为了凝聚力。

相反,我会创建一个具有 execute_command(Command*command) 函数的 UndoRedoStack 类,并使命令尽可能简单。

于 2010-06-24T08:01:02.980 回答
0

基本上帕特里克的建议与大卫的建议相同,这也与我的相同。为此,请使用 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;
};

如果我们退后一步,看看一般问题,而不是直接提出问题,我认为皮特提供了一些非常好的建议。让 Command 负责将自己添加到撤消堆栈中并不是特别灵活。它可以独立于它所在的容器。这些更高级别的职责可能应该是实际容器的一部分,您也可以让其负责执行和撤消命令。

尽管如此,学习NVI应该是非常有帮助的。我见过太多的开发人员编写像这样的纯虚拟接口,因为他们只需要在定义它的每个子类中添加相同的代码,而只需要在一个中心位置实现它。这是一个非常方便的工具,可以添加到您的编程工具箱中。

于 2010-06-24T08:39:09.577 回答
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
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.
        }
    }
}

这使得 OperationManager 不知道每个函数需要什么参数,因此很容易管理不同的函数。

于 2010-06-24T09:21:18.640 回答