本記事ではGoFのデザインパターンのプログラムの振る舞いに関するパターンの一つである「Command」パターンを解説します。このパターンを一言で説明するならば、「一つ一つのコマンドをオブジェクトとして表現し、コマンドの管理を容易にすることが可能」と言えるでしょう。「Command」パターンをサンプルを踏まえて解説します。
「Command」パターンとは
Commandパターンとは、従来のメソッドとして実行したいコマンドを実装するのではなく、実行したいコマンドをオブジェクトとして表現する方法です。このパターンを利用することで、メソッドの呼び出しを抽象化することができ、呼び出し元が実行内容の詳細を気にすることなくコマンドを実行することができます。
Commandパターンは主に5つのパートに分けられます。
登場人物 | 役割 |
---|---|
Command コマンドインターフェース | 実行されるコマンド抽象クラス。このインターフェースにより、すべてのコマンドが同じ仕組みで動くことが保証される。 |
ConcreteCommand 具象コマンドクラス | Commandインターフェースを実装し、具体的な操作を定義する。あくまで定義するだけでコマンドの実行はReceiverに移譲される。 |
Receiver 受け手 | 実際の処理を担うクラス。ConcreteCommandから移譲された処理を実行する。 |
Invoker 呼び出し元 | コマンドを呼び出すクラス。このクラスはどのコマンドを呼び出すかの情報は持っているが、呼び出したコマンドの中身は認知していない。Command内のexcute()を呼び出すだけ。 |
Client クライアント | ConcreteCommandインスタンスとReceiverを関連付け、Invokerにどのコマンドを実行するかの設定を行う。 |
これらの5つのパートをゲームのキャラを操作する例で考えてみましょう。イメージは下記のようになります。わかりにくいのはコントローラーがゲームのキャラを直接操作しているわけではありません。Commandオブジェクトに設定してある動きを、excute()メソッドを利用してキャラクターを動かします。
キャラクターができる動きはCommandオブジェクトとして再定義しています。ConcreateCommandクラスはあくまでCommandをオブジェクトとして扱いたいだけのクラスなので、実際の処理はReciverであるキャラクター内の処理を実行します。
「Command」パターンの書き方
それでは実際にコードに起こして解説していきます。今回は先述のゲーム内のキャラをコマンドで動かすときのコードを実装します。
⭕️ Commandパターンを使用する例 ⭕️
まずはInvokerとしてキャラクターを作成します。図の通りに実装しますので、キャラクターができる行動は「前進する」「後退する」「ジャンプ」「攻撃する」「防御する」の5つになります。それぞれメソッドで定義して、標準出力で適切な文言を出力します。
// キャラクターを表すPlayableCharacterクラス
public class PlayableCharacter {
public void moveForward() {
System.out.println("前へ進め!!");
}
public void moveSpace() {
System.out.println("さがれ!さがれ!");
}
public void jump() {
System.out.println("ジャンプ!");
}
public void attack() {
System.out.println("攻撃だ!");
}
public void defense() {
System.out.println("防御だ!");
}
}
次にキャラクターの行動をコマンドとして表すためのCommandインターフェースを実装します。このインターフェースにはexcute()メソッドとundo()メソッドを用意しています。
// Commandインターフェース
interface Command {
void excute();
void undo();
}
それぞれのメソッドの役割は以下になります。
excute() | コマンド実行の処理を実装する。 |
undo() | コマンドの取り消し処理を実装する |
Commandパターンを利用すると、どのCommandを利用したかの履歴の保持が容易になります。キャラクタークラスのメソッドをそのまま実行すると履歴の保持は難しいですが、Commandパターンはキャラクターの行動をCommandというオブジェクトで管理しているため履歴を簡単に保持できます。この履歴からコマンドの取り消し処理が容易になります。
ではこのCommandインターフェースを実装してキャラクターの「前進する」をコマンド化します。処理が重複している箇所があるため、「前進する」コマンドのみ例で紹介します。他のコマンド化についても同じように実装できます。
// 「前進する」コマンド
public class MoveForwardCommand implements Command {
private final PlayableCharacter playableCharacter;
public MoveForwardCommand(PlayableCharacter playableCharacter) {
this.playableCharacter = playableCharacter;
}
@Override
public void excute() {
playableCharacter.moveForward();
}
@Override
public void undo() {
playableCharacter.moveSpace();
}
}
MoveForwardCommandクラスはCommandを実装しており、キャラクターのクラスであるPlayableCharacterのオブジェクトをフィールドに持っています。excute()メソッド内ではplayableCharacter.moveForward()で前進する処理を実行し、undo()メソッド内ではplayableCharacter.moveSpace()で後退する処理を実行します。
次にこれらのCommandオブジェクトを扱うためのInvokerクラスを実装します。ゲームではコントローラーに位置します。
import java.util.Stack;
public class GameController {
private Stack<Command> commandHistory = new Stack<>();
public void executeCommand(Command command) {
command.excute();
commandHistory.push(command);
}
public void undoLastCommand() {
if (!commandHistory.empty()) {
Command lastCommand = commandHistory.pop();
lastCommand.undo();
} else {
System.out.println("コマンド履歴はありません。");
}
}
}
このクラスにはCommandオブジェクトの履歴を保持しておくための空のStackオブジェクトを持っています。executeCommand()メソッドでは与えられたCommandを実行したのち、commandHistoryの最後部にCommandをpushします。
undoLastCommandではcommandHistoryが空でない場合のみ、最後に追加したCommandをpop()(最後に追加された要素を取り出す)してundo()を実行します。
このクラスで特筆すべきは、「InvokerはCommandの詳しい中身を知っておく必要がない」ということです。与えられたCommandオブジェクトのメソッドを実行すれば良いため、非常に租結合な実装と言えます。新しくCommandが追加されたとしても、Invokerに修正が入ることは全くなく、Commandの中身を気にする必要もありません。
最後にClientですべての設定を行い、実行してみます。
public class Main {
public static void main(String[] args) {
PlayableCharacter mainCharacter = new PlayableCharacter();
GameController controller = new GameController();
Command moveForwarCommand = new MoveForwardCommand(mainCharacter);
Command moveSpaceCommand = new MoveSpaceCommand(mainCharacter);
Command jumpCommand = new JumpCommand(mainCharacter);
Command attackCommand = new AttaackCommand(mainCharacter);
Command defenseCommand = new DefenseCommand(mainCharacter);
controller.executeCommand(moveForwarCommand);
controller.executeCommand(jumpCommand);
controller.executeCommand(attackCommand);
controller.executeCommand(defenseCommand);
controller.executeCommand(moveSpaceCommand);
controller.undoLastCommand();
controller.undoLastCommand();
controller.undoLastCommand();
}
}
>> 実行結果
前へ進め!!
ジャンプ!
攻撃だ!
防御だ!
さがれ!さがれ!
前へ進め!!
defenseコマンドは取り消せません
attackコマンドは取り消せません
キャラクターのオブジェクトとゲームコントローラーのオブジェクト、それぞれCommandオブジェクトを生成ています。それぞれCommandオブジェクトを生成するときに、キャラクターとCommandを紐づけています。
最後にcontroller.executeCommand()で引数にCommandオブジェクトを渡して、キャラクターを動かしています。undoLastCommandコマンドで最後の3回分のコマンドを取り消していますが、攻撃と防御は取り消すことができないため、別で実装したAttaackCommandとDefenseCommandのundo()メソッドでは取り消せない文言の出力を行っています。
✖︎ Commandパターンを使用しない例 ✖︎
次にCommandパターンを利用しなかった場合、どのような弊害が出てしまうか確認します。先ほどと同じシチュエーションで、Commandパターンは利用せず、コントローラーでキャラクターを動かしてみます。
GameControllerクラス内のexecute()に実行したいcommandの文字列を入れて、それに応じたキャラクターの行動を実行します。また実行履歴をコマンドの文字列で保持して、実行履歴からundo()コマンドを実行できるようにします。
class GameController {
private PlayableCharacter character;
private Stack<String> history = new Stack<>();
public GameController(PlayableCharacter character) {
this.character = character;
}
public void execute(String command) {
switch (command) {
case "moveForward":
character.moveForward();
history.push("moveForward");
break;
case "moveSpace":
character.moveSpace();
history.push("moveSpace");
break;
case "jump":
character.jump();
history.push("jump");
break;
case "attack":
character.attack();
history.push("attack");
break;
case "defense":
character.defense();
history.push("defense");
break;
}
}
public void undo() {
if (!history.isEmpty()) {
String lastCommand = history.pop();
switch (lastCommand) {
case "moveForward":
character.moveBackward();
break;
case "moveSpace":
character.moveSpace();
break;
case "jump":
System.out.println("jumpコマンドは取り消せません");
break;
case "attack":
System.out.println("attackコマンドは取り消せません");
break;
case "defense":
System.out.println("defenseコマンドは取り消せません");
break;
}
}
}
}
これでCommandパターンを利用した場合と同じ動作を行います。ではこの実装は何が問題になるでしょうか。問題点は下記になります。
1. 新しい動作の実装が難しくなる
Commandパターンを利用した場合はあたらしくCommandクラスを実装します。利用しない場合は、GameControllerクラスを直接変更する必要があります。これにより、GameControllerを利用するすべてのコードを修正する必要が出てきます。これはシステム全体がオープン/クローズド原則(新しい機能には開いているが、既存のコード修正には閉じているという設計原則)に反するコードになってしまいます。
2. 操作の履歴保持が難しい。
コマンドとしてカプセル化していないため、どの操作が行われたかを追跡するために、大量のコードを実装する必要があります。上記のコードだと、キャラクターができることが増えるたびに、excute()とundo()に、コマンド文字列ごとのif文を追加する必要が出てきます。
3. 柔軟性の欠如
コントローラーは直接キャラクターのメソッドを実行するため、コマンドにおける動作が柔軟に実装できません。例えば、「攻撃をする」の前に「剣を装備する」という動作を実行したい場合、コントローラ内ですべて直接呼び出さなければなりません。CommandパターンではCommandインターフェースを実装するクラス内でキャラクターのメソッドを呼び出すため、コントローラー内には修正を入れることなく、Commandを拡張できます。
Commandパターンを利用しない場合、履歴の保持が難しくなったり、拡張が難しくなったりするため、積極的にCommandパターンを利用しましょう。
まとめ
Commandパターンの利点と欠点は以下になります。
利点 | 欠点 |
---|---|
・操作をカプセル化できる ・オープン/クローズ原則に従う ・操作の履歴管理が容易 ・操作の再利用が容易 ・履歴から戻すことができるためトランザクション管理に適している | ・コマンドの数だけクラス数が増える ・シンプルなアプリケーションには無駄が多い ・インターフェースの仕様に誤りがあると戻せない |
Commandパターンが有効なシチュエーション
- 操作履歴やUndoが必要なシステムの場合
- トランザクションやマクロ処理
- 異なる操作を一元管理したい場合
本記事で利用したサンプルケースのクラス図は下記になります。
最後に「Command」パターンとは「一つ一つのコマンドをオブジェクトとして表現し、コマンドの管理を容易にすることが可能」です。うまく使っていきましょう。
本記事で利用したコードは下記のgit上で公開しております。
コメント