Menu
Coddy logo textTech

コマンドパターン

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

Command パターンは、リクエストをオブジェクトとしてカプセル化することで、異なるリクエストによるクライアントのパラメータ化、操作のキューイング、または取り消し(undo)機能のサポートを可能にします。これは、操作を呼び出すオブジェクトと、その操作の実行方法を知っているオブジェクトを分離します。

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

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

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

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

// 具体的なコマンド(Concrete Commands)
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(); }
};

// Invoker(起動者)
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();
        }
    }
};

各コマンドは、そのレシーバへの参照を保持し、実行時に適切なメソッドを呼び出します。インボーカー(起動側)は、どのようなアクションが発生するかを知りません。単に execute() を呼び出すだけです。コマンドを履歴に保存することで、各コマンドの undo() メソッドを呼び出すことにより、簡単に「元に戻す(undo)」機能を実装できます。

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

challenge icon

チャレンジ

簡単

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

コードは以下の 4 つのファイルに分けて構成します:

  • TextDocument.h: 実際のテキスト内容を保持し、操作するレシーバークラスを作成します。

    TextDocument クラスは、ドキュメントの内容を文字列として保存する必要があります。以下のメソッドを実装してください:

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

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

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

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

    TextEditor クラスは TextDocument への参照を保持し、unique_ptr<Command> の vector を使用して実行されたコマンドの履歴を維持する必要があります。以下を実装してください:

    • executeCommand(std::unique_ptr<Command> cmd) — コマンドを実行し、履歴に追加します
    • undo() — 最新のコマンドを取り消し、履歴から削除します(履歴が空の場合は何もしません)
    • showContent() — 現在のドキュメントの内容を表示します。内容が空の場合は [empty] と表示します
  • main.cpp: 取り消し機能を持つ Command パターンのデモンストレーションを行います。

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

    1. 最初に挿入するテキスト(文字列)
    2. 次に挿入するテキスト(文字列)
    3. 削除する文字数(整数)

    TextDocumentTextEditor を作成します。その後、以下の操作を順番に実行し、各ステップの後に内容を表示してください:

    1. 最初のテキストを挿入し、内容を表示する
    2. 2 番目のテキストを挿入し、内容を表示する
    3. 指定された文字数を削除し、内容を表示する
    4. 1 回取り消しを行い、内容を表示する
    5. もう一度取り消しを行い、内容を表示する

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

Hello
Hello World
Hello Wo
Hello World
Hello

入力が CodeEditor6 の場合:

Code
CodeEditor
Code
CodeEditor
Code

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

チートシート

Command パターンは、リクエストをオブジェクトとしてカプセル化することで、異なるリクエストを持つクライアントをパラメータ化したり、操作をキューに入れたり、取り消し(Undo)機能をサポートしたりすることを可能にします。これにより、操作を呼び出すオブジェクトと、その操作の実行方法を知っているオブジェクトを切り離すことができます。

このパターンには、主に4つの構成要素があります。

  • execute() メソッドを持つ Command インターフェース
  • 具体的なアクションを実装する Concrete Command(具体的なコマンド)
  • 実際の作業を実行する Receiver(受信者)
  • コマンドをトリガーする Invoker(起動者)

基本的な構造:

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

// Command interface
class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() = default;
};

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

// Invoker
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();
        }
    }
};

各コマンドは受信者への参照を保持し、実行時に適切なメソッドを呼び出します。起動者はどのようなアクションが発生するかを知らず、単に execute() を呼び出すだけです。コマンドを履歴に保存することで、各コマンドの undo() メソッドを呼び出して取り消し機能を実装できます。

Command パターンは、操作をキューに入れたり、Undo/Redo を実装したり、リクエストの送信者と処理者を切り離したりする必要がある場合に使用します。

自分で試してみよう

#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回元に戻し(Undo)、内容を表示する
    // 5. もう一度元に戻し(Undo)、内容を表示する
    
    // ヒント: std::make_unique<InsertCommand>(...) と 
    //       std::make_unique<DeleteCommand>(...) を使用してコマンドを作成する

    return 0;
}
quiz icon腕試し

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

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