Abstruct

C#でDecision Table(決定表) を作ってみたので、載せておきます。

決定表は、「条件」と「その条件のときの値」を関連付けて定義できるユーティリティとして便利なのですが、なかなかライブラリとして世の中に転がっておらず、typescriptで以前作ったものをC#で書き換えました。


目次


1. About DecisionTable

決定表については、以下の記事などを参照してください。

2. Folder structure

1
2
3
4
5
├── Program.cs
└── utils
    ├── DecisionTable.cs
    ├── Interfaces.cs
    └── Builder.cs

3. Source Code

3.1. Dicision Table

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
namespace decision_table.utils
{
    /// <summary>
    /// 決定表を表すクラス。条件をチェックして結果を返す
    /// </summary>
    /// <typeparam name="T">入力データの型</typeparam>
    /// <typeparam name="R">戻り値の型</typeparam>
    public class DecisionTable<T, R>
    {
        private readonly List<ConditionChecker<T, R>> conditionCheckers = new List<ConditionChecker<T, R>>();

        /// <summary>
        /// 設定に基づいて条件をチェックし、最初に一致した結果を返す
        /// </summary>
        /// <param name="settings">チェック対象の設定</param>
        /// <returns>条件に一致した結果、または一致しない場合はdefault値</returns>
        public R? Run(T settings)
        {
            var checker = conditionCheckers.FirstOrDefault(x => x.Check(settings));
            return checker != null ? checker.Result : default(R);
        }

        /// <summary>
        /// 条件チェッカーを追加する(内部使用)
        /// </summary>
        /// <param name="checker">追加する条件チェッカー</param>
        internal void AddConditionChecker(ConditionChecker<T, R> checker)
        {
            conditionCheckers.Add(checker);
        }
    }

    /// <summary>
    /// 条件チェックと結果を保持するクラス
    /// </summary>
    /// <typeparam name="T">入力データの型</typeparam>
    /// <typeparam name="R">戻り値の型</typeparam>
    public class ConditionChecker<T, R>
    {
        private R _result = default!;
        private List<Func<T, bool>> _predicates = new List<Func<T, bool>>();

        /// <summary>
        /// 条件が一致した場合の結果
        /// </summary>
        public R Result
        {
            get { return _result; }
            set { _result = value; }
        }

        /// <summary>
        /// チェックする条件のリスト
        /// </summary>
        public List<Func<T, bool>> Predicates
        {
            get { return _predicates; }
            set { _predicates = value; }
        }

        /// <summary>
        /// 全ての条件が満たされているかチェックする
        /// </summary>
        /// <param name="settings">チェック対象の設定</param>
        /// <returns>全ての条件が満たされている場合true</returns>
        public bool Check(T settings)
        {
            return _predicates.All(f => f(settings));
        }
    }
}

3.2. Interfaces.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
namespace decision_table.utils
{
    /// <summary>
    /// DecisionTableの条件を定義する最初のステップのインターフェース
    /// </summary>
    /// <typeparam name="T">入力データの型</typeparam>
    /// <typeparam name="R">戻り値の型</typeparam>
    public interface IDecisionTableConditionBuilder<T, R>
    {
        /// <summary>
        /// 条件を定義する
        /// </summary>
        /// <param name="predicates">条件のリスト(全てがtrueの場合に一致)</param>
        /// <returns>アクションビルダー</returns>
        IDecisionTableActionBuilder<T, R> When(params Func<T, bool>[] predicates);
    }

    /// <summary>
    /// DecisionTableのアクション(結果)を定義するインターフェース
    /// </summary>
    /// <typeparam name="T">入力データの型</typeparam>
    /// <typeparam name="R">戻り値の型</typeparam>
    public interface IDecisionTableActionBuilder<T, R>
    {
        /// <summary>
        /// 追加の条件を定義する
        /// </summary>
        /// <param name="predicates">条件のリスト(全てがtrueの場合に一致)</param>
        /// <returns>アクションビルダー</returns>
        IDecisionTableActionBuilder<T, R> When(params Func<T, bool>[] predicates);

        /// <summary>
        /// 条件が一致した場合の結果を定義する
        /// </summary>
        /// <param name="result">返す結果</param>
        /// <returns>エントリビルダー</returns>
        IDecisionTableEntryBuilder<T, R> Then(R result);
    }

    /// <summary>
    /// DecisionTableのエントリを確定するインターフェース
    /// </summary>
    /// <typeparam name="T">入力データの型</typeparam>
    /// <typeparam name="R">戻り値の型</typeparam>
    public interface IDecisionTableEntryBuilder<T, R>
    {
        /// <summary>
        /// 現在の条件と結果のペアをDecisionTableに追加する
        /// </summary>
        /// <returns>インスタンスビルダー</returns>
        IDecisionTableInstanceBuilder<T, R> Entry();
    }

    /// <summary>
    /// DecisionTableのインスタンスを管理するインターフェース
    /// </summary>
    /// <typeparam name="T">入力データの型</typeparam>
    /// <typeparam name="R">戻り値の型</typeparam>
    public interface IDecisionTableInstanceBuilder<T, R>
    {
        /// <summary>
        /// 新しい条件を追加する
        /// </summary>
        /// <param name="predicates">条件のリスト(全てがtrueの場合に一致)</param>
        /// <returns>アクションビルダー</returns>
        IDecisionTableActionBuilder<T, R> When(params Func<T, bool>[] predicates);

        /// <summary>
        /// DecisionTableを構築して返す
        /// </summary>
        /// <returns>構築されたDecisionTable</returns>
        DecisionTable<T, R> Build();
    }
}

3.3. Builder.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
namespace decision_table.utils
{
    /// <summary>
    /// DecisionTableを構築するためのビルダークラス
    /// Fluent APIパターンを使用して直感的にDecisionTableを作成できる
    /// </summary>
    /// <typeparam name="T">入力データの型</typeparam>
    /// <typeparam name="R">戻り値の型</typeparam>
    public class DecisionTableBuilder<T, R> : 
        IDecisionTableConditionBuilder<T, R>,
        IDecisionTableActionBuilder<T, R>,
        IDecisionTableEntryBuilder<T, R>,
        IDecisionTableInstanceBuilder<T, R>
    {
        private readonly DecisionTable<T, R> decisionTable = new DecisionTable<T, R>();
        private ConditionChecker<T, R> tmpConditionChecker = null!;

        /// <summary>
        /// 条件を定義する
        /// </summary>
        /// <param name="predicates">条件のリスト(全てがtrueの場合に一致)</param>
        /// <returns>アクションビルダー</returns>
        public IDecisionTableActionBuilder<T, R> When(params Func<T, bool>[] predicates)
        {
            tmpConditionChecker = new ConditionChecker<T, R>();
            tmpConditionChecker.Predicates = predicates.ToList();
            return this;
        }

        /// <summary>
        /// 条件が一致した場合の結果を定義する
        /// </summary>
        /// <param name="result">返す結果</param>
        /// <returns>エントリビルダー</returns>
        public IDecisionTableEntryBuilder<T, R> Then(R result)
        {
            tmpConditionChecker.Result = result;
            return this;
        }

        /// <summary>
        /// 現在の条件と結果のペアをDecisionTableに追加する
        /// </summary>
        /// <returns>インスタンスビルダー</returns>
        public IDecisionTableInstanceBuilder<T, R> Entry()
        {
            decisionTable.AddConditionChecker(tmpConditionChecker);
            return this;
        }

        /// <summary>
        /// DecisionTableを構築して返す
        /// </summary>
        /// <returns>構築されたDecisionTable</returns>
        public DecisionTable<T, R> Build()
        {
            return decisionTable;
        }
    }

    /// <summary>
    /// DecisionTableBuilderを作成するためのファクトリクラス
    /// </summary>
    public static class DecisionTableFactory
    {
        /// <summary>
        /// 新しいDecisionTableBuilderを作成する
        /// </summary>
        /// <typeparam name="T">入力データの型</typeparam>
        /// <typeparam name="R">戻り値の型</typeparam>
        /// <returns>新しいDecisionTableBuilder</returns>
        public static IDecisionTableConditionBuilder<T, R> CreateBuilder<T, R>()
        {
            return new DecisionTableBuilder<T, R>();
        }
    }
}

3.4. Program.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
using decision_table.utils;

namespace decision_table
{
    // テスト用のStateクラス
    public class State
    {
        public int Field1 { get; set; }
        public string Field2 { get; set; } = string.Empty;
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("DecisionTable テスト開始");
            
            // DecisionTableの作成
            var builder = DecisionTableFactory.CreateBuilder<State, string>();
            var decisionTable = builder
                .When(
                    s => s.Field1 == 100,
                    s => s.Field2 == "test"
                )
                .Then("return sample")
                .Entry()
                .When(
                    s => s.Field1 > 50,
                    s => s.Field2 == "hello"
                )
                .Then("return hello")
                .Entry()
                .When(s => s.Field1 < 10)
                .Then("return small")
                .Entry()
                .Build();

            // テストケース1: 完全一致
            Console.WriteLine("=== テストケース1: 完全一致 ===");
            var result1 = decisionTable.Run(new State { Field1 = 100, Field2 = "test" });
            Console.WriteLine($"Input: Field1=100, Field2=\"test\" => Result: {result1 ?? "null"}");

            // テストケース2: 2番目の条件に一致
            Console.WriteLine("\n=== テストケース2: 2番目の条件に一致 ===");
            var result2 = decisionTable.Run(new State { Field1 = 60, Field2 = "hello" });
            Console.WriteLine($"Input: Field1=60, Field2=\"hello\" => Result: {result2 ?? "null"}");

            // テストケース3: 3番目の条件に一致
            Console.WriteLine("\n=== テストケース3: 3番目の条件に一致 ===");
            var result3 = decisionTable.Run(new State { Field1 = 5, Field2 = "anything" });
            Console.WriteLine($"Input: Field1=5, Field2=\"anything\" => Result: {result3 ?? "null"}");

            // テストケース4: どの条件にも一致しない
            Console.WriteLine("\n=== テストケース4: どの条件にも一致しない ===");
            var result4 = decisionTable.Run(new State { Field1 = 30, Field2 = "nomatch" });
            Console.WriteLine($"Input: Field1=30, Field2=\"nomatch\" => Result: {result4 ?? "null"}");

            // 異なる戻り値型のテスト
            Console.WriteLine("\n=== 数値を返すDecisionTable ===");
            var numericTable = DecisionTableFactory.CreateBuilder<State, int>()
                .When(s => s.Field1 > 100)
                .Then(1000)
                .Entry()
                .When(s => s.Field1 > 50)
                .Then(500)
                .Entry()
                .When(s => s.Field1 > 0)
                .Then(100)
                .Entry()
                .Build();

            var numResult = numericTable.Run(new State { Field1 = 75, Field2 = "test" });
            Console.WriteLine($"Input: Field1=75 => Numeric Result: {numResult}");

            Console.WriteLine("\nテスト完了");
        }
    }
}

4. Execution Result

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
❯ dotnet run
DecisionTable テスト開始
=== テストケース1: 完全一致 ===
Input: Field1=100, Field2="test" => Result: return sample

=== テストケース2: 2番目の条件に一致 ===
Input: Field1=60, Field2="hello" => Result: return hello

=== テストケース3: 3番目の条件に一致 ===
Input: Field1=5, Field2="anything" => Result: return small

=== テストケース4: どの条件にも一致しない ===
Input: Field1=30, Field2="nomatch" => Result: null

=== 数値を返すDecisionTable ===
Input: Field1=75 => Numeric Result: 500

テスト完了