はじめに
今回は命令をクラスにするCommand
パターンを紹介したいと思います。
Commandパターンの定義
Wikipediaにかなりわかりやすくまとめられていました。
Commandパターンでは何かリクエストを実行する際、単純に処理を実行するのではなく、次のステップを踏む。
- 処理をメソッドとして内包するCommandクラスの定義
- Commandオブジェクトの生成
- Command.Execute()のコールによるリクエスト実行
すなわちリクエストを「手順書」の定義・生成とその「実行」に段階分けするパターンをとる。
登場人物
名前 | 意味 |
---|---|
Command | 命令のインターフェイスを定義する役 |
ConcreteCommand | Commandの具象クラス |
Receiver | Commandが実行するときに対象となる役。命令の受け取り手。 |
Invoker | 命令を実行する役 |
Client | ConcreteCommand を生成し、その際にReceiver を割り当てる役 |
Commnad
Command
インターフェイスがExecute
メソッドのみを定義します。
public interface Command { void Execute(); }
Receiver
Receiver
クラスはCommand
を実行する対象(命令の受け取り手)のことです。
今回はIReceiver
インターフェイスというインターフェイスを定義しています。ここでインターフェイスを定義するメリットはモックを用いたテストであったり,複数のReceiver
があっても容易に対応できる点だと思います。
public interface IReceiver { // インターフェイスにどんなメソッドを定義するか・インターフェイスを分けるか等は状況に応じて void Receive1(); void Receive2(); } public class Receiver : IReceiver { public void Receive1() { Console.WriteLine("Commandを受け取りました。"); } public void Receive2() { Console.WriteLine("Commandを受け取ったよ。"); } }
ConcreteCommand
Command
インターフェイスを実装するConcreteCommand
を実装していきます。
public class ConcreteCommand1 : Command { private IReceiver _receiver; public ConcreteCommand1(IReceiver receiver) { _receiver = receiver; } public void Execute() { // 実装の中身についてはReceiverに委譲している // この箇所に細かい処理の流れを書く場合もある _receiver.Receive1(); } } public class ConcreteCommand2 : Command { private IReceiver _receiver; public ConcreteCommand2(IReceiver receiver) => _receiver = receiver; public void Execute() => _receiver.Receive2(); }
Execute
メソッドについてですが、Wikipediaに以下のような説明がなされています。
全ての処理をCommand内に記述するパターン、あるいはcommandインスタンス生成時に処理対象(Receiver)を受け取りcommand.Execute()時にreceiver.action()コールのみをおこなう、すなわち処理をReceiverに委譲してCommandは繋ぎに徹するパターンもある。
Command
パターン=全ての処理をCommand
内に記述するみたいな印象がある方も多い??かもしれませんが、私がみてきた中ではReceiver
に委譲しているコードの方が多い気がします。
ただどちらが優れているといったものではないと思うので、自身のプロダクトにあっているやり方で実装してみてはどうでしょうか。
Invoker
次にCommand
を実行する役であるInvoker
を実装していきます。
public class Invoker { private Queue<Command> _commands = new Queue<Command>(); public void AddCommand(Command command) => _commands.Enqueue(command); public void Execute() { while(_commands.Count != 0) { var command = _commands.Dequeue(); command.Execute(); } } }
よくある例としては,キュー(Queue
)というデータ構造を用いて実装されます。またInvoker
に履歴をつけてみたり,Undo
機能をつけてみたりなんかもすることができます。
Client
class Client { private static void Main(string[] args) { Receiver receiver = new Receiver(); Invoker invoker = new Invoker(); // Commandを生成 invoker.AddCommand(new ConcreteCommand1(receiver)); invoker.AddCommand(new ConcreteCommand1(receiver)); invoker.AddCommand(new ConcreteCommand2(receiver)); invoker.Execute(); } }
Client
がReceiver
・Invoker
への参照を何かしらの手段で取得する必要があります。これには複数の手法が考えられますので,こちらも状況に応じてといったところでしょうか。
Unity
なら今回のサンプルの手法の他,SerializeField
・Singleton
・DI Container
など複数考えられます。
また今回はClient
内でInvoker.Execute
を実行していますが,これも必ずしもClient
の責務であるとは私は思いません。
Unity
ならライフサイクルを利用するのもありだと思います。
さいごに
Command
パターンではClient
とInvoker
が切り離されていますが,これにより力を発揮できる場面も多くあります。(逆にClient
とInvoker
が一緒になっている場合もある)
例えばゲームの敵のAIを作成するときに、AI(ここでいうClient
)はCommand
を生成してキューにため、コマンドを実行する役である的(Invoker
)が任意のタイミングで実行していくような設計にするときです。
こうすることでAIを容易に変えられたり,どの敵(攻撃パターンや移動方法等が様々にあったとしても)に対しても容易に対応することができます。
ゲームを作る上ではかなり有用なデザインパターンであると思うので、是非うまく活用してみてください。
ではまた。