はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】命令をクラスにするCommandパターンを学ぶ

はじめに

今回は命令をクラスにするCommandパターンを紹介したいと思います。

f:id:hanaaaaaachiru:20210309173627p:plain
クラス図

Commandパターンの定義

Wikipediaにかなりわかりやすくまとめられていました。

Commandパターンでは何かリクエストを実行する際、単純に処理を実行するのではなく、次のステップを踏む。

  1. 処理をメソッドとして内包するCommandクラスの定義
  2. Commandオブジェクトの生成
  3. Command.Execute()のコールによるリクエスト実行

すなわちリクエストを「手順書」の定義・生成とその「実行」に段階分けするパターンをとる。

Command パターン - Wikipedia

登場人物

名前 意味
Command 命令のインターフェイスを定義する役
ConcreteCommand Commandの具象クラス
Receiver Commandが実行するときに対象となる役。命令の受け取り手。
Invoker 命令を実行する役
Client ConcreteCommandを生成し、その際にReceiverを割り当てる役
f:id:hanaaaaaachiru:20210309173627p:plain
クラス図
f:id:hanaaaaaachiru:20210310124508p:plain
シーケンス図

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 パターン - Wikipedia

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

ClientReceiverInvokerへの参照を何かしらの手段で取得する必要があります。これには複数の手法が考えられますので,こちらも状況に応じてといったところでしょうか。

Unityなら今回のサンプルの手法の他,SerializeFieldSingletonDI Container など複数考えられます。

また今回はClient内でInvoker.Executeを実行していますが,これも必ずしもClientの責務であるとは私は思いません。

Unityならライフサイクルを利用するのもありだと思います。

さいごに

CommandパターンではClientInvokerが切り離されていますが,これにより力を発揮できる場面も多くあります。(逆にClientInvokerが一緒になっている場合もある)

例えばゲームの敵のAIを作成するときに、AI(ここでいうClient)はCommandを生成してキューにため、コマンドを実行する役である的(Invoker)が任意のタイミングで実行していくような設計にするときです。

こうすることでAIを容易に変えられたり,どの敵(攻撃パターンや移動方法等が様々にあったとしても)に対しても容易に対応することができます。

ゲームを作る上ではかなり有用なデザインパターンであると思うので、是非うまく活用してみてください。

ではまた。