本文へスキップ
BecomeCoder

C#デザインパターンコース · 第1章 デザインパターン · レッスン10

振る舞い③:Command / Chain of Responsibility

ブラウザで完結

導入

Commandは「操作をオブジェクトとして表現」——実行・取り消し・キュー化ができるように。Chain of Responsibilityは「処理役を数珠つなぎにし、扱える者が処理」——承認フローやミドルウェアの構造です。

いつもは calc.Add(50, 30); のようにその場で呼んで、その場で実行します。Commandはこれを「足し算をしてほしい」という注文票に変え、注文票をあとで・まとめて・順番に実行できるようにします。レストランの伝票を思い浮かべてください。お客さん(クライアント)が伝票を書き、ホール係(インヴォーカー)が伝票を溜めて厨房に流し、料理を作るのは調理人(レシーバー)です。

図解

Commandには5人の登場人物がいます。役割ごとにファイルを分けると、それぞれが何をする係なのかがはっきりします。

役割このサンプルでのファイル / クラス仕事
インターフェース(Command)ICalcCommand.cs「Execute() を持つ」という共通の形だけを決める
レシーバー(Receiver)Receiver.cs実際の処理そのもの。足し算・引き算をする本体
実装クラス(ConcreteCommand)AddCalcCommand.cs / SubtractCalcCommand.cs注文票。何を・どの値でやるかを持ち、レシーバーに委譲する
インヴォーカー(Invoker)Invoker.cs注文票を溜めて(キュー)、順番に Execute() する
クライアント(Client)Program.cs上の4つを組み立てて動かす側
classDiagram
    class ICalcCommand {
        <<interface>>
        +Execute()
    }
    class AddCalcCommand {
        -Receiver _receiver
        +Execute()
    }
    class SubtractCalcCommand {
        -Receiver _receiver
        +Execute()
    }
    class Receiver {
        +AddAction(first, second)
        +SubtractAction(first, second)
    }
    class Invoker {
        -Queue~ICalcCommand~ _calcCommands
        +AddCommand(command)
        +ExecuteAll()
    }
    class Program {
        +Main()
    }
    ICalcCommand <|.. AddCalcCommand
    ICalcCommand <|.. SubtractCalcCommand
    AddCalcCommand --> Receiver
    SubtractCalcCommand --> Receiver
    Invoker o-- ICalcCommand
    Program --> Invoker
    Program --> Receiver

大事なのは、インヴォーカーは ICalcCommand しか知らないことです。足し算なのか引き算なのか、値がいくつなのかを一切知らないまま「溜めて、順に実行する」だけができます。だから後から掛け算コマンドを足しても、インヴォーカーは1行も変わりません。

サンプル:Command(役割ごとにファイルを分ける)

実務では役割ごとに別ファイルに置きます。下のエディタも同じ6ファイル構成になっていて、上のタブで切り替えられます。全ファイルはまとめてコンパイルされるので、そのまま「▶ 実行」を押せば動きます。

1. インターフェース(ICalcCommand.cs)。すべてのコマンドの土台。「Execute() を持っている」以外は何も決めません。

public interface ICalcCommand
{
    void Execute();
}

2. レシーバー(Receiver.cs)。実際の処理内容はここだけにあります。Commandパターンを知らなくても書ける、ふつうのクラスです。

// 実際に仕事をする人(調理人)
public class Receiver
{
    public void AddAction(double first, double second)
    {
        Console.WriteLine($"和: {first + second}");
    }

    public void SubtractAction(double first, double second)
    {
        Console.WriteLine($"差: {first - second}");
    }
}

3. 実装クラス(AddCalcCommand.cs)。注文票そのもの。「誰に(レシーバー)」「どの値で」やってもらうかをコンストラクタで受け取って持っておき、Execute() で呼び出すだけです。自分では計算しません。

// 「和を出して」という注文票
public class AddCalcCommand : ICalcCommand
{
    private readonly Receiver _receiver;
    private readonly double _first;
    private readonly double _second;

    public AddCalcCommand(Receiver receiver, double first, double second)
    {
        _receiver = receiver;
        _first = first;
        _second = second;
    }

    public void Execute()
    {
        _receiver.AddAction(_first, _second);   // 処理はレシーバーに任せる
    }
}

4. もう1つの実装クラス(SubtractCalcCommand.cs)。形はまったく同じで、呼ぶメソッドだけが違います。「操作の種類=クラスの種類=ファイルの数」になるのがCommandの特徴です。

// 「差を出して」という注文票
public class SubtractCalcCommand : ICalcCommand
{
    private readonly Receiver _receiver;
    private readonly double _first;
    private readonly double _second;

    public SubtractCalcCommand(Receiver receiver, double first, double second)
    {
        _receiver = receiver;
        _first = first;
        _second = second;
    }

    public void Execute()
    {
        _receiver.SubtractAction(_first, _second);
    }
}

5. インヴォーカー(Invoker.cs)。注文票を Queue に溜めて、先に入れたものから実行します。中身を知らないので、溜める・並べる・あとで流すが自由にできます。

// 注文票を溜めて順に流す人(ホール係)
public class Invoker
{
    private readonly Queue<ICalcCommand> _calcCommands = new Queue<ICalcCommand>();

    public void AddCommand(ICalcCommand command)
    {
        _calcCommands.Enqueue(command);        // 溜めるだけ。まだ実行しない
    }

    public void ExecuteAll()
    {
        while (_calcCommands.Count > 0)
        {
            var current = _calcCommands.Dequeue();
            current.Execute();                 // 入れた順に実行
        }
    }
}

AddCommand した時点ではまだ何も起きず、ExecuteAll() で初めて動く——この「頼む」と「実行する」のズレこそがCommandの効き目です。ズレがあるから、履歴に残す・あとで取り消す・順番を入れ替える・別スレッドで流す、といったことが後付けできます。

サンプル:Chain of Responsibility

こちらは「扱えなければ次の人へ回す」パターン。承認額に応じて担当者が変わる稟議フローがそのまま形になります。担当者1人=クラス1つ=ファイル1つです。

flowchart LR
    R["要求"] --> H1["担当A<br/>扱える?"]
    H1 -->|無理| H2["担当B<br/>扱える?"]
    H2 -->|無理| H3["担当C<br/>処理"]

6. 承認者の共通の形(Approver.cs)。「次の人」を持てるようにするのが連鎖の肝です。

public abstract class Approver
{
    protected Approver? Next;
    protected Approver(Approver? next) => Next = next;
    public abstract void Approve(int amount);
}

7. 各担当者(Leader.cs / Manager.cs)。承認段階を増やすときはファイルを1つ足して、つなぎ方を変えるだけです。

public class Leader : Approver
{
    public Leader(Approver? next) : base(next) { }

    public override void Approve(int amount)
    {
        if (amount <= 100000) Console.WriteLine($"課長が承認: {amount}円");
        else Next?.Approve(amount);   // 扱えなければ次へ回す
    }
}
public class Manager : Approver
{
    public Manager(Approver? next) : base(next) { }

    public override void Approve(int amount) => Console.WriteLine($"部長が承認: {amount}円");
}

8. クライアント(Program.cs)。役者をそろえて注文票を積み、最後に一気に実行します。承認の連鎖もここで組み立てます。

// ---- Command ----
var firstNum = 50;
var secondNum = 30;

var receiver = new Receiver();
var invoker = new Invoker();

invoker.AddCommand(new AddCalcCommand(receiver, firstNum, secondNum));
invoker.AddCommand(new SubtractCalcCommand(receiver, firstNum, secondNum));

Console.WriteLine($"{firstNum}と{secondNum}の和と差は?");
invoker.ExecuteAll();
// 50と30の和と差は?
// 和: 80
// 差: 20

// ---- Chain of Responsibility ----
var chain = new Leader(new Manager(null));
chain.Approve(80000);    // 課長が承認: 80000円
chain.Approve(300000);   // 部長が承認: 300000円
  • Command: 操作をExecute()を持つオブジェクトにし、履歴・キュー・取り消しに対応できる
  • Chain of Responsibility: 処理役を連結し、「自分が扱えなければ次へ回す」
  • どちらも「呼び出し側」と「処理内容」を切り離す

やってみよう

下のエディタはこのサンプルの全ファイルが入った状態です。タブを切り替えながら、次の順で書き換えてみましょう。役割分担のありがたみが体感できます。

  1. Program.cs の AddCommand の2行を入れ替える → 実行順だけが変わり、他のファイルは無傷。
  2. 掛け算コマンドを足す(Receiver.cs に MultiplyAction を追加し、AddCalcCommand.cs をまねた実装クラスを書く)→ Invoker.cs は1行も変えずに掛け算が流せる。
  3. ExecuteAll() の呼び出しを消す → 何も計算されないことを確認。「頼む」と「実行する」が別物だとわかります。
  4. Leader.cs の上限額(100000)を変える → 承認が課長で止まるか部長へ回るかが変わり、Program.cs は無変更。

演習

ICommand cmd = new GreetCommand("Taro");
cmd.Execute();   // こんにちは、Taro

// TODO: Execute() で "こんにちは、名前" を出力する GreetCommand を定義してください
// (実装クラス=注文票。名前をコンストラクタで受け取り、フィールドに持っておきます)
interface ICommand { void Execute(); }
___
  • 期待される出力: こんにちは、Taro
ヒント1を見る

名前をフィールドに持ち、Executeで出力します

ヒント2を見る

class GreetCommand : ICommand { private readonly string _name; public GreetCommand(string name) => _name = name; public void Execute() => Console.WriteLine($"こんにちは、{_name}"); }

まとめ

  • Commandは操作をオブジェクト化し、実行・履歴・取り消しを可能にする
  • 登場人物は インターフェース/レシーバー/実装クラス/インヴォーカー/クライアント の5つ。ファイルを分けると役割がはっきりする
  • インヴォーカーはインターフェースしか知らないので、コマンドを増やしても壊れない
  • Chain of Responsibilityは処理役を連結し順に委ねる
  • どちらも呼び出しと処理内容を分離する

次回: 手順の骨組みと反復のTemplate Method/Iteratorです。

実際に動かしてみよう

このレッスンのサンプルは、実務と同じように役割ごとの .cs ファイルへ分けてあります。下のエディタは最初からその複数ファイルが入った状態で、上のタブでファイルを切り替えられます。そのまま「▶ 実行」を押せば全ファイルをまとめてコンパイルして動かせるので、クラスを足したりファイルをまたいで書き換えたりしながら進めましょう(演習は本文のコードをコピーして Program.cs のタブに貼り付ければ試せます)。

C# — ブラウザ内で実行

ブラウザ内でC#を動かす環境を読み込みます(初回のみ数秒)。
スクロールして表示された時点でも自動で読み込まれます。

次のレッスンへ →