DI コンテナでStrategy パターンの実装:IEnumerable実装 から IDictionary 最適化へ

に公開

はじめに

本記事は、.NET の DI コンテナを用いたストラテジーパターン実装の備忘録です。
.NETのDIコンテナ Microsoft.Extensions.DependencyInjection でストラテジーパターンを実装した際、同一インターフェースを IEnumerable で注入出来ることが分かったので、 FirstOrDefault で検索する形で実装していましたが、O(n)の計算量がかかるというので、O(1)で検索できる Dictionary で実装してみたところ実現出来たので、それらの実装方法をまとめることにしました。

実行環境

  • Win11
  • コンソールアプリ
  • .NET 8
  • ライブラリ
    • Microsoft.Extensions.DependencyInjection

実装の概要

  • 入力ごとに異なる「処理戦略(Strategy)」を適用
  • キーには enum を使用(型安全で扱いやすい)
  • Dictionary<StrategyKey, IStrategy> によって戦略を O(1) で高速に取得
  • 戦略が見つからない場合は、DefaultStrategy を適用

Strategyの識別に使うenumの定義

public enum StrategyKey
{
    TypeA,
    TypeB,
    TypeC,
    Unknown
}

Strategyのインターフェースと実装

// Strategy インターフェース
public interface IStrategy
{
    StrategyKey Key { get; }
    string Execute(string input);
}

// DefaultStrategy インターフェース
public interface IDefaultStrategy : IStrategy { }

それぞれの処理戦略の実装

// 通常のストラテジー
public class StrategyA : IStrategy
{
    public StrategyKey Key => StrategyKey.TypeA;
    public string Execute(string input) => $"[A Strategy] Input: {input}";
}

public class StrategyB : IStrategy
{
    public StrategyKey Key => StrategyKey.TypeB;
    public string Execute(string input) => $"[B Strategy] Input: {input}";
}

public class StrategyC : IStrategy
{
    public StrategyKey Key => StrategyKey.TypeC;
    public string Execute(string input) => $"[C Strategy] Input: {input}";
}


// デフォルトストラテジー
public class DefaultStrategy : IDefaultStrategy
{
    public StrategyKey Key => StrategyKey.Unknown;
    public string Execute(string input) => $"[Default Strategy] Input: {input}";
}

案1 : IEnumerable パターン

ContentHandler の実装

public interface IContentHandler
{
    string HandleContent(List<(StrategyKey key, string input)> inputs);
}

public class ContentHandler(
    // IEnumerableでインジェクション
    IEnumerable<IStrategy> _strategies,
    IDefaultStrategy _defaultStrategy) : IContentHandler
{
    public string HandleContent(List<(StrategyKey key, string input)> inputs)
    {
        var result = new List<string>();
        
        foreach (var (key, input) in inputs)
        {
            // FirstOrDefaultでストラテジーを取得する
            var strategy = _strategies.FirstOrDefault(s => s.Key == key)
                ?? _defaultStrategy;

            result.Add(strategy.Execute(input));
        }
        
        return string.Join("\n", result);
    }
}

DIコンテナへの登録と処理の実行

using Microsoft.Extensions.DependencyInjection;

// Program.cs などの DI コンテナ設定
var services = new ServiceCollection();

// Strategy を複数登録
services.AddSingleton<IStrategy, StrategyA>();
services.AddSingleton<IStrategy, StrategyB>();
services.AddSingleton<IStrategy, StrategyC>();
services.AddSingleton<IDefaultStrategy, DefaultStrategy>();

// ContentGenerator を登録
services.AddScoped<IContentHandler, ContentHandler>();

var inputs = new List<(StrategyKey, string)>
{
    (StrategyKey.TypeA, "1. First input"),
    (StrategyKey.TypeB, "2. Second input"),
    (StrategyKey.TypeC, "3. Third input"),
    (StrategyKey.TypeA, "4. Fourth input"),
    (StrategyKey.TypeC, "5. Fifth input"),
    ((StrategyKey)999, "6. Unknown input") // 存在しないキー
};

// サービスプロバイダーの構築
var serviceProvider = services.BuildServiceProvider();
var handler = serviceProvider .GetRequiredService<IContentHandler>();
// 実行
var output = handler.HandleContent(inputs);

Console.WriteLine("=== 1. FirstOrDefault Pattern Strategy Output ===");
Console.WriteLine(output);

実行結果

=== 1. FirstOrDefault Pattern Strategy Output ===
[A Strategy] Input: 1. First input
[B Strategy] Input: 2. Second input
[C Strategy] Input: 3. Third input
[A Strategy] Input: 4. Fourth input
[C Strategy] Input: 5. Fifth input
[Default Strategy] Input: 6. Unknown input

https://gist.github.com/rendya2501/a4302075527dfeac5c7852e42c01f235#file-program-cs

案2 : IDictionaryパターン

ContentHandler の実装

public interface IContentHandler
{
    string HandleContent(List<(StrategyKey key, string input)> inputs);
}

public class ContentHandler(
    // IDictionaryでインジェクション
    IDictionary<StrategyKey, IStrategy> _strategyMap,
    IDefaultStrategy _defaultStrategy) : IContentHandler
{
    public string HandleContent(List<(StrategyKey key, string input)> inputs)
    {
        var result = new List<string>();
        
        foreach (var (key, input) in inputs)
        {
            // TryGetValueでキーでストラテジーを検索する
            var strategy = _strategyMap.TryGetValue(key, out var foundStrategy)
                ? foundStrategy
                : _defaultStrategy;

            result.Add(strategy.Execute(input));
        }
        
        return string.Join("\n", result);
    }
}

DIコンテナへの登録と処理の実行

using Microsoft.Extensions.DependencyInjection;

// Program.cs などの DI コンテナ設定
var services = new ServiceCollection();

// Strategy を複数登録
services.AddSingleton<IStrategy, StrategyA>();
services.AddSingleton<IStrategy, StrategyB>();
services.AddSingleton<IStrategy, StrategyC>();
services.AddSingleton<IDefaultStrategy, DefaultStrategy>();

// Dictionary を生成して DI コンテナに登録
services.AddSingleton<IDictionary<StrategyKey, IStrategy>>(provider =>
{
    var strategies = provider.GetServices<IStrategy>();
    return strategies.ToDictionary(s => s.Key);
});

// ContentGenerator を登録
services.AddScoped<IContentHandler, ContentHandler>();

var inputs = new List<(StrategyKey, string)>
{
    (StrategyKey.TypeA, "1. First input"),
    (StrategyKey.TypeB, "2. Second input"),
    (StrategyKey.TypeC, "3. Third input"),
    (StrategyKey.TypeA, "4. Fourth input"),
    (StrategyKey.TypeC, "5. Fifth input"),
    ((StrategyKey)999, "6. Unknown input") // 存在しないキー
};

// サービスプロバイダーの構築
var serviceProvider = services.BuildServiceProvider();
var handler = serviceProvider .GetRequiredService<IContentHandler>();
// 実行
var output = handler.HandleContent(inputs);

Console.WriteLine("=== 2. Dictionary Pattern Strategy Output ===");
Console.WriteLine(output);

実行結果

=== 2. Dictionary Pattern Strategy Output ===
[A Strategy] Input: 1. First input
[B Strategy] Input: 2. Second input
[C Strategy] Input: 3. Third input
[A Strategy] Input: 4. Fourth input
[C Strategy] Input: 5. Fifth input
[Default Strategy] Input: 6. Unknown input

https://gist.github.com/rendya2501/e87db27d13e7ec0075a1651faba87fc8

案3 : IDictionary拡張 パターン

IDictionary では GetValueOrDefault メソッドが存在しないので IDictionary インターフェースに拡張メソッドを定義して GetValueOrDefault メソッドを実装して使用できるようにしたパターン。
やっていることは案2と変わりないです。
DIを IDictionary 型ではなく Dictionary 型にするとやらなくても良いことではあるが、割と感動したのでメモしておく。


public interface IContentHandler
{
    string HandleContent(List<(StrategyKey key, string input)> inputs);
}

public class ContentHandler(
    IDictionary<StrategyKey, IStrategy> _strategyMap,
    IDefaultStrategy _defaultStrategy) : IContentHandler
{
    public string HandleContent(List<(StrategyKey key, string input)> inputs)
    {
        var result = new List<string>();
        
        foreach (var (key, input) in inputs)
        {
            // IDictionary型でGetValueOrDefaultメソッドを使用する
            var strategy = _strategyMap.GetValueOrDefault(key, _defaultStrategy);

            result.Add(strategy.Execute(input));
        }
        
        return string.Join("\n", result);
    }
}

// 拡張メソッドを定義
public static class DictionaryExtensions
{
    public static TValue GetValueOrDefault<TKey, TValue>(
        this IDictionary<TKey, TValue> dictionary,
        TKey key,
        TValue defaultValue = default!)
    {
        return dictionary.TryGetValue(key, out var value) ? value : defaultValue;
    }
}

https://gist.github.com/rendya2501/99b8bc8268ad61831b00ca80a657eec9

メリットとデメリット(一般論として)

メリット

  • 検索速度の向上:O(n) → O(1)
  • 拡張性:Strategy を追加・削除しやすい
  • テストしやすい:依存注入によりテスト可能性向上

デメリット

  • 初期化コスト:Dictionary 構築時に O(n)
  • 構成の複雑化:Factory や Dictionary の管理が必要

結論(適用判断の指針)

条件 推奨手法
Strategy 数が少ない(<10) FirstOrDefault でも十分
Strategy 数が多い(>100) Dictionary による高速化推奨
頻繁に検索が発生する処理 Dictionary を推奨
将来的な拡張や可読性を重視する場合 Factory + Dictionary の構成を採用

小規模なアプリや一時的な用途であれば FirstOrDefault の簡潔さがメリットですが、大規模・高頻度のケースでは Dictionary を用いた方式が有効です。設計の柔軟性を重視する場合は拡張メソッドや Factory を組み合わせた構成も検討すると良いでしょう。

GitHubで編集を提案

Discussion