Menu
Coddy logo textTech

Commandパターン

CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 96/104。

Command パターンはリクエストをオブジェクトとしてカプセル化し、異なるリクエストでクライアントをパラメーター化したり、操作をキューに入れたり、元に戻す機能をサポートしたりできるようにします。これは、操作を呼び出すオブジェクトと、その操作の実行方法を把握しているオブジェクトを分離します。

このパターンには、4つの主要なコンポーネントがあります。Commandインターフェースとそのexecute()メソッド、特定のアクションを実装するConcrete Commands、actualな作業を実行するReceiver、そしてcommandsをトリガーするInvokerです。

#include <iostream>
#include <memory>
#include <vector>

// レシーバー - 操作の実行方法を知っている
class Light {
public:
    void turnOn() { std::cout << "Light is ON\n"; }
    void turnOff() { std::cout << "Light is OFF\n"; }
};

// コマンドインターフェース
class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() = default;
};

// 具象コマンド
class LightOnCommand : public Command {
    Light& light;
public:
    LightOnCommand(Light& l) : light(l) {}
    void execute() override { light.turnOn(); }
    void undo() override { light.turnOff(); }
};

class LightOffCommand : public Command {
    Light& light;
public:
    LightOffCommand(Light& l) : light(l) {}
    void execute() override { light.turnOff(); }
    void undo() override { light.turnOn(); }
};

// インボーカー
class RemoteControl {
    std::vector<std::unique_ptr<Command>> history;
public:
    void pressButton(std::unique_ptr<Command> cmd) {
        cmd->execute();
        history.push_back(std::move(cmd));
    }
    
    void pressUndo() {
        if (!history.empty()) {
            history.back()->undo();
            history.pop_back();
        }
    }
};

各 command は receiver への参照を保持し、実行されると適切なメソッドを呼び出します。invoker はどのような action が発生するかを知りません。単に execute() を呼び出すだけです。command を history に保存しておけば、各 command の undo() メソッドを呼び出すことで、簡単に undo を実装できます。

操作をキューに入れたり、元に戻す/やり直しを実装したり、リクエストの送信元をハンドラーから分離したりする必要がある場合は、Commandを使用します。

challenge icon

チャレンジ

簡単

Command パターンを使用して、取り消し機能を備えたテキストエディターを構築しましょう。テキストの挿入や削除などの編集操作をコマンドオブジェクトとしてカプセル化し、実際のテキストエディターと同じように、ユーザーが操作を実行した後、逆の順序で取り消せるシステムを作成します。

コードを4つのファイルに分けて構成します。

  • TextDocument.h: 実際のテキスト内容を保持・操作する receiver クラスを作成します。

    TextDocument クラスでは、ドキュメントの内容を文字列として保持します。次のメソッドを実装してください。

    • insertText(const std::string& text): 内容の末尾にテキストを追加します
    • deleteText(int count): 内容の末尾から最後の count 文字を削除します(count が内容の長さを超える場合は何もしません)
    • getContent(): 現在の内容を const 参照として返します
  • Command.h: コマンドインターフェースと具体的なコマンドクラスを定義します。

    純粋仮想メソッド execute()undo()、および仮想デストラクタを持つ抽象 Command クラスを作成します。

    次に、2つの具体的なコマンドを実装します。

    • InsertCommand: TextDocument への参照と、挿入する文字列を受け取ります。実行されるとテキストを挿入します。取り消されると、挿入された文字数と同じ数の文字を削除します。
    • DeleteCommand: TextDocument への参照と、削除する文字数を受け取ります。取り消し時に復元できるよう、削除したテキストを保存する必要があります。実行されると末尾から文字を削除します。取り消されると、保存したテキストを再び挿入します。
  • TextEditor.h: コマンドの実行と履歴を管理する invoker を作成します。

    TextEditor クラスでは、TextDocument への参照を保持し、unique_ptr<Command> の vector を使用して実行済みコマンドの履歴を管理します。次を実装してください。

    • executeCommand(std::unique_ptr<Command> cmd): コマンドを実行し、履歴に追加します
    • undo(): 最も新しいコマンドを取り消し、履歴から削除します(履歴が空の場合は何もしません)
    • showContent(): 現在のドキュメント内容を出力します。内容が空の場合は [empty] を出力します
  • main.cpp: 取り消し機能を備えた Command パターンの動作を示します。

    3つの入力を読み取ります。

    1. 挿入する1つ目のテキスト(文字列)
    2. 挿入する2つ目のテキスト(文字列)
    3. 削除する文字数(整数)

    TextDocumentTextEditor を作成します。その後、各ステップの内容を表示しながら、次の操作を順番に実行します。

    1. 1つ目のテキストを挿入し、その後内容を表示する
    2. 2つ目のテキストを挿入し、その後内容を表示する
    3. 指定された文字数を削除し、その後内容を表示する
    4. 1回取り消し、その後内容を表示する
    5. もう1回取り消し、その後内容を表示する

たとえば、入力が Hello World3 の場合:

Hello
Hello World
Hello Wo
Hello World
Hello

入力が CodeEditor6 の場合:

Code
CodeEditor
Code
CodeEditor
Code

各コマンドが、自身をどのように逆戻りさせるかを把握していることに注目してください。DeleteCommand は削除した内容を記憶して復元できるようにし、InsertCommand は取り消し時に削除すべき文字数を正確に把握しています。これが Command パターンの力です。操作が、保存・実行・逆転可能な第一級オブジェクトになります。

自分で試してみよう

#include <iostream>
#include <string>
#include <memory>
#include "TextDocument.h"
#include "Command.h"
#include "TextEditor.h"

int main() {
    // 入力を読み取る
    std::string firstText;
    std::string secondText;
    int deleteCount;
    
    std::getline(std::cin, firstText);
    std::getline(std::cin, secondText);
    std::cin >> deleteCount;

    // TODO: TextDocument と TextEditor を作成する
    
    // TODO: 順番に操作を実行し、各操作後に内容を表示する:
    // 1. 最初のテキストを挿入し、内容を表示する
    // 2. 2番目のテキストを挿入し、内容を表示する
    // 3. 指定された文字数を削除し、内容を表示する
    // 4. 1回元に戻し、内容を表示する
    // 5. もう1回元に戻し、内容を表示する
    
    // ヒント: std::make_unique<InsertCommand>(...) と 
    //       std::make_unique<DeleteCommand>(...) を使用してコマンドを作成する

    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: C++オンラインコンパイラ