質問

私は実装しようとしています コマンドデザインパターン, 、しかし、私は概念的な問題を偶然見つけています。以下の例のような基本クラスといくつかのサブクラスがあるとしましょう。

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などのサブクラスを構築すると、基本クラスのコンストラクターがAutomaticalyと呼ばれるため、そこに「登録」するために呼び出しを追加できることを知っています。 Operator()()が呼び出されるまで、私が登録を呼びたくないもの。

これどうやってするの?私のデザインにはやや欠陥があると思いますが、この作品を作る方法が本当にわかりません。

役に立ちましたか?

解決

NVI(非仮想的なインターフェイス)Idiomの恩恵を受けることができるように見えます。そこにはのインターフェイスがあります 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を検索すると、より多くのより良い説明が得られます。

補遺:オリジナル 記事 彼がコンセプトを紹介するハーブ・サッターによって(まだ名前が付けられていません)

他のヒント

オペレーターを2つの異なる方法で分割します。たとえば、Execute and ExecuteImpl(正直に言うと、()演算子はあまり好きではありません)。 command :: execute nonvirtual、and command :: executeimpl pure virtualを実行し、登録を実行し、次のようにexecuteimplと呼びます。

class Command
   {
   public:
      ResultType execute()
         {
         ... // do registration
         return executeImpl();
         }
   protected:
      virtual ResultType executeImpl() = 0;
   };

class SomeCommand
   {
   protected:
      virtual ResultType executeImpl();
   };

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

それらを混合したい場合は、3つのテンプレートメソッドがあり、それぞれが2つのスタックを取得します(または、作成時に動作するスタックへの参照が必要です)。関数。ただし、これらの3つの方法がある場合、コマンド上のパブリック機能を呼び出す以外に実際には何もしないことがわかり、コマンドの他の部分では使用されていないので、次にコードをリファクタリングするときに候補者になります凝集のため。

代わりに、execute_command(command*command)関数を備えたundoredostackクラスを作成し、コマンドをできるだけシンプルにします。

基本的に、パトリックの提案は、私のものと同じであるデビッドの提案と同じです。この目的のために、NVI(非仮想的なインターフェイスIdiom)を使用します。純粋な仮想インターフェイスには、あらゆる種類の集中制御がありません。代わりに、すべてのコマンドが継承する別の抽象的な基本クラスを作成することもできますが、なぜわざわざなのでしょうか?

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;
};

一歩下がって、即座に質問されるのではなく、一般的な問題を見ると、ピートは非常に良いアドバイスを提供していると思います。 UNDOスタックにそれ自体を追加するコマンドを担当することは、特に柔軟ではありません。それは、それが存在する容器から独立している可能性があります。これらの高レベルの責任は、おそらく実際のコンテナの一部である必要があります。これは、コマンドの実行と元に戻す責任を担当することもできます。

それにもかかわらず、NVIを研究することは非常に役立つはずです。私は、あまりにも多くの開発者が、1つの中央で実装する必要があるときにそれを定義するすべてのサブクラスに同じコードを追加するためだけに、歴史的な利点からこのような純粋な仮想インターフェイスを書くのを見てきました。プログラミングツールボックスに追加するための非常に便利なツールです。

私はかつて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;
    }
}

関数が実行される場合はいつでも、現在選択されているオブジェクトまたは作業が必要なものに基づいています。注:私の場合、アクティブな関数として設定された入力デバイスからの移動術によって計算されていたため、各オブジェクトが移動するべき量の詳細を送信する必要はありませんでした。
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は、各関数が必要とする議論がどのようなものであるかを認識できず、さまざまな機能を簡単に管理できました。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top