デザインパターンを3つの教材で学習|Commandパターン
学習ソース
デザインパターンを複数の視点で学ぶため、以下の書籍とUdemyで学習している。パターン毎に上から順に進めている。
-
Game Programming Patterns ソフトウェア開発の問題解決メニュー
(Game Programming Patterns (English Edition))- ゲームを題材としているが、Web開発や組込み等、どのようなジャンルにも使える知識が掲載されている本。ゲームが例のためイメージしやすいと評判。デザインパターンは万能ではないということも説明している。言語はC++だが、C++ユーザーでなくても読めるように配慮されている。
-
Java言語で学ぶデザインパターン入門第3版
- デザインパターンを学ぶなら、まずこれが最初に上がるような名著。QiitaやZenn、技術ブログ等でも多数紹介されている。最近第3版が出版された。言語はJavaだがJavaユーザーでない自分にも読みやすい。
-
Udemy「Python デザインパターンマスター講座~Pythonの基本文法、コーディング規約、命名規約、プログラミング技術~」
- Pythonでデザインパターンを学べる教材は少ない中見つけた講義。コーディング規約やSOLID等のプログラミングの基本も扱っている。
Commandパターン(コマンドパターン)とは?
操作をリクエストするクラスと、操作を実行するクラスを分離する。
分離することで疎結合となり、操作の記録や順序付けを行う仲介オブジェクトを追加可能なパターン。
代表的な応用例は、Undo/Redo/Macro機能。
Game Programming Patternsに書かれている原書の定義
リクエストをオブジェクトとしてカプセル化する。これによって、異なるリクエスト / キュー / ログリクエストをもったクライアントのパラメータ化、および取り消し可能なオペレーションが実装できるようになる。
また、
四人組も後に「コマンドパターンは、コールバックのオブジェクト指向における代替品である」と語っています。
と記載がある。
Java言語で学ぶデザインパターン入門には、
Commandは、Eventと呼ばれる場合もあります。「イベント駆動型プログラミング」で使われる「イベント」と同じ意味です。
との記載がある。
Commandパターンが使える場面
以下のような機能を実装したいときに使えると理解した。
- GUIソフトのUndo(元に戻す)/Redo(やり直す)機能の実現(お絵描きソフトで、線を書いた後に元に戻したり、やり直す場合等)
(drwa.io) - RPGゲームにおけるターン制のコマンド選択のUndo機能の実現(ドラクエ等で、最初は一人目でホイミをしようと思っていたが、やはり戻ってぼうぎょに変更する場合等)
- ネットワークを経由したマルチプレーヤーゲームの実現。ゲームのプレーヤーの入力コマンドをネットワーク越しに送信することで、他のマシンはそれを受信してコマンドを再現することができる。
- ゲームAIで、攻撃的なコマンドを生成させたり、回復系のコマンドを生成させて自動操作の実現。
- ゲームの再現表示。フレーム毎にゲームの全ての状態を記録する方法もあるが、それだとメモリを消費しすぎるため、多くのゲームでは、フレーム毎にすべてのゲーム要素が実行したコマンドを記録しておき、それを実行させることで再現表示をしているらしい。
- リモコン(TVやゲーム等)の各ボタンの機能をCommandパターンで実装することで、各ボタンの機能を変更したり、ボタンを追加したり、元に戻すといったことが簡単に実現できる
- プログレスバー。一連のコマンドを順番に実行する場合、各Commandオブジェクトが予測処理時間を返すメソッドを持っていると、全体の処理時間を簡単に予測することができる。
- インストーラやデータベースのトランザクション的振る舞い。Undo機能とUndoに失敗した時に前の状態に戻す必要がある場合に使える。
- マクロ機能。あらゆるユーザー操作をCommandオブジェクトとしておけば、Commandオブジェクトのリストを保持するだけで操作の流れを記録できる。Commandオブジェクトを同じ順序で実行することで、同じ操作を再生することができる。
Commandパターンを使わない場合の例
Game Programming Patternsより。ゲームのボタンと動作の対応づけの単純な実装は以下となる。
void InputHandler::handleInput()
{
if (isPressed(BUTTON_X)) jump();
else if (isPressed(BUTTON_Y)) fireGun();
else if (isPressed(BUTTON_A)) swapWeapon();
else if (isPressed(BUTTON_B)) lurchIneffectively(); // 意味もなくふらつく
}
これだと、ユーザーからの入力に対してゲーム内の動作を固定的に結びつけてしまう。しかし、多くのゲームではボタンと動作の対応をユーザーが設定可能になっている。その設定を可能にするには、jump()やfireGun()の直接呼び出しを、入れ替えができるものにする必要がある。つまり、ゲーム内の動作を表すオブジェクトが必要になる。ここでCommandパターンが登場する。
Commandパターンを使った例
Game Programming Patternsより。
class Command
{
public:
virtual ~Command() {}
virtual void execute() = 0;
};
class JumpCommand : public Command
{
pubic:
virtual void execute() { jump(); }
};
//以下、同様...
class InputHandler
{
public:
void handleInput();
// 以下、コマンドをボタンに割り当てるメソッドなど
private:
Command* buttonX_;
Command* buttonY_;
Command* buttonA_;
Command* buttonB_;
};
void InputHandler::handleInput()
{
if (isPressed(BUTTON_X)) buttonX_->execute();
else if (isPressed(BUTTON_Y)) buttonY_->execute();
else if (isPressed(BUTTON_A)) buttonA_->execute();
else if (isPressed(BUTTON_B)) buttonB_->execute();
}
これで間接参照の層ができた。本当はこの後Undoの説明もあるが、ここでは省略する。
Commandパターンの登場人物
Java言語で学ぶデザインパターン入門より。
- Command(命令)役: 命令のAPIを定義。
- ConcreteCommand(具体的命令)役: Command役のAPIを実際に実装。
- Receiver(受信者)役: Command役が命令を実行するときに対象となる役。命令の受取手。
- Client(依頼者)役: ConcreteCommand役を生成し、その際にReceiver役を割り当てる。
- Invoker(起動者)役: 命令の実行を開始する役。Command役で定義されているAPIを呼び出す。
Undoの実装例
Udemyより。各Commandクラスでundoメソッドを実装しておき、それをInvokerであるRemoteControllerクラスから実行することでundoを実現する。以下の例はundoをリストで持っていないため完璧ではないが、このように実装すればundoが実現できる。
from abc import ABC, abstractmethod
from enum import Enum
class CommandNumber(Enum):
LIGHT = 0
TV = 1
GAME = 2
# Receiver
class Light:
def __init__(self, name):
self.__name = name
def on_Light(self):
print(f'Light: Turn on {self.__name}')
def off_Light(self):
print(f'Light: turn off {self.__name}')
# Command
class Command(ABC):
@abstractmethod
def execute(self):
pass
def undo(self):
pass
# ConcreteCommand
class NoCommand(Command):
def execute(self):
pass
def undo(self):
pass
class LightOnCommand(Command):
def __init__(self, light: Light):
self.__light = light
def execute(self):
self.__light.on_Light()
def undo(self):
self.__light.off_Light()
class LightOffCommand(Command):
def __init__(self, light: Light):
self.__light = light
def execute(self):
self.__light.off_Light()
def undo(self):
self.__light.on_Light()
# Invoker
class RemoteController:
def __init__(self):
self.__on_commands = [NoCommand()] * len(CommandNumber)
self.__off_commands = [NoCommand()] * len(CommandNumber)
self.__undo_command = NoCommand()
def set_command(self, number, on_command: Command, off_command: Command):
self.__on_commands[number] = on_command
self.__off_commands[number] = off_command
def on_command(self, number):
self.__on_commands[number].execute()
self.__undo_command = self.__on_commands[number]
def off_command(self, number):
self.__off_commands[number].execute()
self.__undo_command = self.__off_commands[number]
def undo_command(self):
self.__undo_command.undo()
これをmainで下記の通り実行する。
light = Light('My Light')
remote_controller = RemoteController()
remote_controller.set_command(CommandNumber.LIGHT.value, LightOnCommand(light), LightOffCommand(light))
remote_controller.on_command(CommandNumber.LIGHT.value)
remote_controller.off_command(CommandNumber.LIGHT.value)
remote_controller.undo_command()
結果は、
Light: Turn on My Light
Light: turn off My Light
Light: Turn on My Light <-- offのundoのためon
Udemyでは取り上げられていなかったが、このままではUndoは1回しかできないため、deque(両端キュー)を使って改良したのが以下。
from collections import deque
# Invoker
class RemoteController:
def __init__(self):
self.__on_commands = [NoCommand()] * len(CommandNumber)
self.__off_commands = [NoCommand()] * len(CommandNumber)
self.__undo_commands = deque()
def set_command(self, number, on_command: Command, off_command: Command):
self.__on_commands[number] = on_command
self.__off_commands[number] = off_command
def on_command(self, number):
self.__on_commands[number].execute()
self.__undo_commands.append(self.__on_commands[number])
def off_command(self, number):
self.__off_commands[number].execute()
self.__undo_commands.append(self.__off_commands[number])
def undo_command(self):
if len(self.__undo_commands) <= 0:
print('cannot undo')
return
print('Undo')
self.__undo_commands.pop().undo()
これを下記通り実行すると、Undoが機能していることがわかる。
remote_controller.off_command(CommandNumber.LIGHT.value)
remote_controller.undo_command()
remote_controller.undo_command()
remote_controller.undo_command()
結果
Light: Turn on My Light
Light: turn off My Light
Undo
Light: Turn on My Light
Undo
Light: turn off My Light
cannot undo
Redo機能の実装例
上記UdemyのPython例を拡張して、Redo機能にも対応させてみる。
# Command
class Command(ABC):
@abstractmethod
def execute(self):
pass
def undo(self):
pass
def redo(self):
pass
# ConcreteCommand
class NoCommand(Command):
def execute(self):
pass
def undo(self):
pass
def redo(self):
pass
class LightOnCommand(Command):
def __init__(self, light: Light):
self.__light = light
def execute(self):
self.__light.on_Light()
def undo(self):
self.__light.off_Light()
def redo(self):
self.execute()
class LightOffCommand(Command):
def __init__(self, light: Light):
self.__light = light
def execute(self):
self.__light.off_Light()
def undo(self):
self.__light.on_Light()
def redo(self):
self.execute()
# Invoker
class RemoteController:
def __init__(self):
self.__on_commands = [NoCommand()] * len(CommandNumber)
self.__off_commands = [NoCommand()] * len(CommandNumber)
self.__undo_commands = deque()
self.__redo_commands = deque()
def set_command(self, number, on_command: Command, off_command: Command):
self.__on_commands[number] = on_command
self.__off_commands[number] = off_command
def on_command(self, number):
self.__on_commands[number].execute()
self.__undo_commands.append(self.__on_commands[number])
def off_command(self, number):
self.__off_commands[number].execute()
self.__undo_commands.append(self.__off_commands[number])
def undo_command(self):
if len(self.__undo_commands) <= 0:
print('cannot undo')
return
print('Undo')
undone_command = self.__undo_commands.pop()
undone_command.undo()
self.__redo_commands.append(undone_command)
def redo_command(self):
if len(self.__redo_commands) <= 0:
print('cannot redo')
return
print('Redo')
redone_command = self.__redo_commands.pop()
redone_command.redo()
self.__undo_commands.append(redone_command)
これを下記コードで実行する。
remote_controller.on_command(CommandNumber.LIGHT.value)
remote_controller.off_command(CommandNumber.LIGHT.value)
remote_controller.undo_command()
remote_controller.undo_command()
remote_controller.undo_command()
remote_controller.redo_command()
remote_controller.redo_command()
remote_controller.redo_command()
remote_controller.undo_command()
remote_controller.undo_command()
remote_controller.undo_command()
結果は、
Light: Turn on My Light
Light: turn off My Light
Undo 3 times
Undo
Light: Turn on My Light
Undo
Light: turn off My Light
cannot undo
Redo 3 times
Redo
Light: Turn on My Light
Redo
Light: turn off My Light
cannot redo
Undo 3 times
Undo
Light: Turn on My Light
Undo
Light: turn off My Light
cannot undo
確かに、UndoしたものがRedoすることで復元されている。
通常実行(execute)とredoによるやり直しで動作を変更する可能性が考えられるため、redoメソッド内部でexecute実行することとした。しかし、まだこれでは完全では無い。undoしたあとに全く別の処理をした場合にはredoできなくするような対策が必要。このことは、Game Progamming Patternsに以下の記載があった。
プレーヤーが「取り消し」を選択すると、カレントコマンドの取り消しを行い、カレンとポインタを後退させます。プレーヤーが「再実行」を選択するとポインタを前進させてその指しているコマンドを実行します。プレーヤーがいくつかの取り消しを行なったあとに新しいコマンドを選択した場合には、カレントコマンドよりもあとのものをすべて削除します。
そこで、下記の通り、なんらかの動作を実行するとredo dequeをclearするようにする。
# Invoker
class RemoteController:
//省略
def on_command(self, number):
self.__on_commands[number].execute()
self.__undo_commands.append(self.__on_commands[number])
self.__redo_commands.clear()
def off_command(self, number):
self.__off_commands[number].execute()
self.__undo_commands.append(self.__off_commands[number])
self.__redo_commands.clear()
これにより、undo以外の動作をするとredoができなくなる。
上記例では、Commandクラスにundo, redoを実装したが、Java言語で学ぶデザインパターン入門では、Commandクラスはexecuteメソッドのみで、MacroCommandクラスを以下の通り作成することでundo機能を実現していた。MacroCommandのインスタンスとしてhistoryを作成し、Receiverの描画処理でhistoryに蓄えられた動作を順に実行していくことで実現している。そのため、historyの中から最後の動作をpop()で消せばundo処理が実現される。動作を記録し、後で再現させるマクロ機能やゲームAI等ではこちらのクラスを使った方が、まとまりのあるコマンドを1つのクラスのインスタンスとして持たせることができるので良いと思う。
import java.util.ArrayDeque;
import java.util.Deque;
public class MacroCommand implements Command {
private Deque<Command> commands = new ArrayDeque<>();
@override
public void execute() {
for (Command cmd: commands) {
cmd.execute();
}
}
public void append(Command cmd) {
if (cmd == this) {
throw new IllegalArgumentException("infinite loop caused by append");
commands.push(cmd);
}
public void undo() {
if (!commands.isEmpty()) {
commands.pop();
}
}
public void clear() {
commands.clear();
}
}
感想
今回、初めてCommandパターンを勉強したが、使い勝手の良いパターンであると感じた。当然、クラス数が増える等のデメリットはあるため、簡単なことしかしない所には適用しない方が良い場合もあると思うが、複数のコマンドやイベントを扱う場合には再利用性、拡張性の高い構造にできると感じた。Mementoパターンと関連があるようなので、またそのパターンを学習した後に加筆修正はしようと思う。
3種類のソースから学習し、他のサイトも参考にすることで、1つのソースだけからでは気付けなかった所も気付けたため、複数の本やUdemyで学習することは、デザインパターン初学者の方にはオススメする。本当は、原書やオブジェクト指向のこころも読むとより理解が進むと思うが、抽象的でなかなか理解するのが難しいとのことなので、まずはデザインパターンをいわゆる「完全に理解した」と思った後にでも読もうと思う。ひとまずはこの3つのソースから学習することとする。いずれ有名なHead Firstは読もうと思っている。2022年6月に第2版が出たのでちょうど良さそうだとは感じている。
参考文献
-
Game Programming Patterns ソフトウェア開発の問題解決メニュー
(Game Programming Patterns (English Edition)) - Java言語で学ぶデザインパターン入門第3版
- Udemy「Python デザインパターンマスター講座~Pythonの基本文法、コーディング規約、命名規約、プログラミング技術~」
- Wikipedia「Command パターン」
- Command パターンを用いてコマンド操作のリプレイを実装する【Game Development Pattern With Unity】
- 【Commandパターン】GUIイベント処理や履歴管理で用いるデザインパターン【コード例:Java】
Discussion