⛓️

【C#】Chain-of-responsibility パターン

2023/04/15に公開

はじめに

やっぱりオブジェクト指向プログラミングがよくわからない!キョです。
今回は続いて、Chain-of-responsibility パターンを紹介したいと思います。
※続いてとしても、前回はいつでしたっけ?

1.Chain-of-responsibility パターンとは

Wikiの説明は以下になります。

Chain-of-responsibility パターン, CoR パターンは、オブジェクト指向設計におけるデザインパターンの一つであり、一つのコマンドオブジェクトと一連の処理オブジェクト (processing objects) から構成される。各処理オブジェクトは、処理できるコマンドオブジェクトの種類と、自身が処理できないコマンドオブジェクトをチェーン内の次の処理オブジェクトに渡す方法を記述する情報を保持する。また、新たな処理オブジェクトをチェーンの最後に追加する機構を備える。

https://ja.wikipedia.org/wiki/Chain_of_Responsibility_パターン
「一つのコマンドオブジェクトと一連の処理オブジェクト (processing objects) から構成」
ふむふむ。。。やっぱりふわっとしか頭に入らないですね。
それではサンプルコードを作って、理解してみましょう。

2.サンプルコード

前回と同じくまずは作るプログラムの要件を定義します。
今回はこの前Youtubeで見たすごく面白いCMから思いついた「稟議書システム(ハンコのリレー!)」というプログラムを作りたいと思います。
※気になる方はYoutubeで探してみてくださいww

要件

稟議書の申請をするシステム

  1. 申請内容設定可能
    1. 申請の内容説明文
    2. 必要金額
  2. 承認者を1名以上指定できる
  3. 承認者は申請内容によって、許可するかを判断可能

どうでしょう。簡単だと思いますので、コード書いてみます!

最初に作ったプログラム(もちろんデザインパターンなし!)

まずは、承認者を列挙型で定義します。
そして、稟議書の「内容、金額、最終承認者」を設定できるようにします。
最後は各承認者はどう判断するかは一つのメソッドに定義しました。
※今回は試しにクラス名と変数名を全部日本語で定義しました。(前から試したかったです)

var 稟議書 = new 稟議書("部長最高!", 50, 承認者.社長);
稟議書.承認();

public enum 承認者
{
    リーダー,
    課長,
    部長,
    社長
}

public class 稟議書
{
    public string 内容 { get; private set; }
    public decimal 金額 { get; private set; }
    public 承認者 最終承認者 { get; private set; }

    public 稟議書(string 内容, decimal 金額, 承認者 最終承認者)
    {
        this.内容 = 内容;
        this.金額 = 金額;
        this.最終承認者 = 最終承認者;
    }

    public void 承認()
    {
        var 現在承認者 = 承認者.リーダー;
        while (現在承認者 <= 最終承認者)
        {
            if (承認(現在承認者))
            {
                現在承認者++;
            }
            else
            {
                Console.WriteLine("承認できませんでした。");
                return;
            }
        }
        Console.WriteLine("承認できました。");
    }

    private bool 承認(承認者 承認者)
    {
        switch (承認者)
        {
            case 承認者.リーダー:
                Console.WriteLine("リーダーが承認しています。");
                    
                if (金額 >= 100)
                {
                    Console.WriteLine("金額が100円以上なので、承認しません。");
                    return false;
                }

                Console.WriteLine("金額が100円未満なので、承認します。");
                return true;

            case 承認者.課長:
                Console.WriteLine("課長が承認しています。");
                
                if (金額 >= 1000)
                {
                    Console.WriteLine("金額が1000円以上なので、承認しません。");
                    return false;
                }

                Console.WriteLine("金額が1000円未満なので、承認します。");
                return true;

            case 承認者.部長:
                Console.WriteLine("部長が承認しています。");
                
                if (内容.Contains("部長最高!") == false)
                {
                    Console.WriteLine("内容に部長最高!が含まれていないので、承認しません。");
                    return false;
                }

                Console.WriteLine("内容に部長最高!が含まれているので、承認します。");
                return true;

            case 承認者.社長:
                Console.WriteLine("社長が承認しています。");
                Console.WriteLine("ダメ!!!");
                return false;
        }

        Console.WriteLine("承認者なかったので、承認できませんでした。");
        return false;
    }
}

実行結果は下になります。

リーダーが承認しています。
金額が100円未満なので、承認します。
課長が承認しています。
金額が1000円未満なので、承認します。
部長が承認しています。
内容に部長最高!が含まれているので、承認します。
社長が承認しています。
ダメ!!!
承認できませんでした。

正常に動いていますね。
でも、以下の問題がありますね。。。

  1. 承認の順番は固定になっている
  2. 最終承認者までの承認者すべての承認を受けないといけない
  3. 承認操作は一つのメソッドにまとめていて、承認者が多くなるとメソッドも一緒に膨大化になってしまう
  4. 稟議書クラスがどれぐらいの承認者が存在するかを知らないといけない
  5. 稟議書クラスが承認操作に関する知識を持っている

それでは、デザインパターンを利用して、リファクタリングしてみましょう。

Chain-of-responsibility パターンを利用したリファクタリング

リファクタリング内容は以下になります。

  1. 承認者をクラスに抽出して、各承認者の承認操作をクラス内に移動
  2. 必要の承認者をコンストラクタで設定できるようにする
  3. 承認者の承認順番を自由に設定できるようにする

リファクタリング後のコードは以下になります。

var 稟議書 = new 稟議書("部長最高!", 50, new リーダー(new 課長(new 部長(new 社長(null)))));
稟議書.承認();

public abstract class 承認者
{
    public 承認者 次の承認者 { get; set; }

    public 承認者(承認者 次の承認者)
    {
        this.次の承認者 = 次の承認者;
    }
    protected abstract bool 承認処理(稟議書 稟議書);

    public bool 承認(稟議書 稟議書)
    {
        var result = this.承認処理(稟議書);
        if (result == false) return false;

        return this.次の承認者が承認(稟議書);
    }

    public bool 次の承認者が承認(稟議書 稟議書)
    {
        return this.次の承認者?.承認(稟議書) ?? true;
    }
}

public class リーダー : 承認者
{
    public リーダー(承認者 次の承認者) : base(次の承認者) { }

    protected override bool 承認処理(稟議書 稟議書)
    {
        Console.WriteLine("リーダーが承認しています。");
        if (稟議書.金額 >= 100)
        {
            Console.WriteLine("金額が100円以上なので、承認しません。");
            return false;
        }

        Console.WriteLine("金額が100円未満なので、承認します。");
        return true;
    }
}

public class 課長 : 承認者
{
    public 課長(承認者 次の承認者) : base(次の承認者) { }

    protected override bool 承認処理(稟議書 稟議書)
    {
        Console.WriteLine("課長が承認しています。");

        if (稟議書.金額 >= 1000)
        {
            Console.WriteLine("金額が1000円以上なので、承認しません。");
            return false;
        }

        Console.WriteLine("金額が1000円未満なので、承認します。");
        return true;
    }
}

public class 部長 : 承認者
{
    public 部長(承認者 次の承認者) : base(次の承認者) { }

    protected override bool 承認処理(稟議書 稟議書)

    {
        Console.WriteLine("部長が承認しています。");

        if (稟議書.内容.Contains("部長最高!") == false)
        {
            Console.WriteLine("内容に部長最高!が含まれていないので、承認しません。");
            return false;
        }

        Console.WriteLine("内容に部長最高!が含まれているので、承認します。");
        return true;
    }
}

public class 社長 : 承認者
{
    public 社長(承認者 次の承認者) : base(次の承認者) { }

    protected override bool 承認処理(稟議書 稟議書)
    {
        Console.WriteLine("社長が承認しています。");
        Console.WriteLine("ダメ!!!");
        return false;
    }
}

public class 稟議書
{
    public string 内容 { get; private set; }
    public decimal 金額 { get; private set; }
    public 承認者 承認者 { get; private set; }

    public 稟議書(string 内容, decimal 金額, 承認者 承認者)
    {
        this.内容 = 内容;
        this.金額 = 金額;
        this.承認者 = 承認者;
    }

    public void 承認()
    {
        if (this.承認者.承認(this) == false)
        {
            Console.WriteLine("承認できませんでした。");
            return;
        }

        Console.WriteLine("承認できました。");
    }
}

実行結果は以下です。

リーダーが承認しています。
金額が100円未満なので、承認します。
課長が承認しています。
金額が1000円未満なので、承認します。
部長が承認しています。
内容に部長最高!が含まれているので、承認します。
社長が承認しています。
ダメ!!!
承認できませんでした。

正常に動きましたね。
試しに、承認者の順番と人数を変更してみます。

var 稟議書 = new 稟議書("部長いない!", 50, new リーダー(new 課長(new 社長(null))));
稟議書.承認();
リーダーが承認しています。
金額が100円未満なので、承認します。
課長が承認しています。
金額が1000円未満なので、承認します。
社長が承認しています。
ダメ!!!
承認できませんでした。

いけましたね。

ここで、

  • 稟議書クラスがWikiの説明中の「一つのコマンドクラス」
  • 承認者クラス群がWikiの説明中の「一連の処理オブジェクト」

になりますね。

結果は、稟議書は承認操作に関する知識が知らなくてもいいし、
承認者がどれぐらい存在するかも知る必要がありません。
そして、稟議書作成時も、自由に必要承認者と承認順番の指定ができるようになりました。
最後は、新しい承認者を追加する時も、新しい承認者クラスを一つ追加するだけで、
既存処理に影響なく、修正が完了できますね。

終わり

どうでしょうか
これで、Chain-of-responsibility パターンについて簡単に説明してみました。
誰かのお役に立てれば幸いです。

参考サイト

https://ja.wikipedia.org/wiki/Chain_of_Responsibility_パターン

Discussion