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インスタンスで演算子()が呼び出されるたびに、コマンドのregisterメソッドを呼び出して* thisをスタックに追加したいと思います(主に元に戻す目的で)。SomeCommand :: operator()()から「register」を呼び出さないようにしたいのですが、自動的に呼び出されるようにします(どういうわけか;-))

SomeCommandなどのサブクラスを作成すると、基本クラスのコンストラクターが自動的に呼び出されるので、そこに「登録」の呼び出しを追加できることを知っています。operator()()が呼び出されるまでregisterを呼び出したくないもの。

これどうやってするの?私のデザインには多少の欠陥があると思いますが、これを機能させる方法がよくわかりません。

4

5 に答える 5

28

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 を検索すると、より多くのより良い説明が得られます。

補遺: Herb Sutter による元の記事で、彼はコンセプトを紹介しています (まだ名前はありません)

于 2010-06-24T07:39:59.253 に答える
6

演算子を 2 つの異なるメソッド (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

元に戻すとやり直しを行う「通常の」アプリケーションであると仮定すると、スタックの管理と、スタック上の要素によって実行されるアクションを混在させようとはしません。複数の取り消しチェーンがある場合 (たとえば、複数のタブが開いている場合)、または do-undo-redo の場合、コマンドは自分自身を undo に追加するか、自分自身を redo から undo に移動するかを認識しなければならない場合、非常に複雑になります。または元に戻すからやり直しに移動します。また、コマンドをテストするには、元に戻す/やり直しスタックをモックする必要があることも意味します。

それらを混在させたい場合は、3 つのテンプレート メソッドがあり、それぞれが 2 つのスタックを取得し (または、コマンド オブジェクトが作成時に動作するスタックへの参照を持っている必要があります)、それぞれが移動または追加を実行し、次に呼び出します。関数。しかし、これらの 3 つのメソッドがある場合は、コマンドでパブリック関数を呼び出す以外に実際には何も実行せず、コマンドの他の部分では使用されないことがわかります。そのため、次にコードをリファクタリングするときに候補になります。結束のために。

代わりに、execute_command(Command*command) 関数を持つ UndoRedoStack クラスを作成し、コマンドをできるだけ単純なままにします。

于 2010-06-24T08:01:02.980 に答える
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は各関数に必要な引数を認識しなくなり、さまざまな関数を簡単に管理できるようになりました。

于 2010-06-24T09:21:18.640 に答える
0

基本的に、Patrick の提案は David の提案と同じであり、私の提案も同じです。この目的には、NVI (非仮想インターフェイスイディオム) を使用します。純粋な仮想インターフェイスには、あらゆる種類の集中制御がありません。別の方法として、すべてのコマンドが継承する別の抽象基本クラスを作成することもできますが、わざわざする必要はありません。

NVI が望ましい理由の詳細については、Herb Sutter による C++ Coding Standards を参照してください。そこで彼は、オーバーライド可能なコードをパブリック インターフェイス コードから厳密に分離するために、すべてのパブリック関数を非仮想化することを提案しています (オーバーライド可能であってはならないため、常に何らかの集中制御を行い、インストルメンテーションを事前/事後的に追加できます)。状態チェック、およびその他必要なもの)。

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 を学習することは非常に役立つはずです。あまりにも多くの開発者が、このような純粋な仮想インターフェイスを作成するのを見てきました。これまでの利点から、それを定義するすべてのサブクラスに同じコードを追加するだけで済みました。これは、プログラミング ツールボックスに追加するのに非常に便利なツールです。

于 2010-06-24T08:39:09.577 に答える