はじめに

C#でDIをする方法について学びました。

DIとは

DI (Dependency injection)は、「依存性の注入」と訳され、
SOLID原則の「Dependency Inversion Principle:依存性逆転の原則」に基づくデザインパターンで、
疎結合にするための手段です。

SOLIDとは、5つのプラクティスの頭文字とった原則集です。

  • Single Responsibility Principle:単一責務の原則
  • Open-Closed Principle:開放閉鎖の原則
  • Liskov Substitution Principle:リスコフの置換原則
  • Interface Segregation Principle:インターフェイス分離の原則
  • Dependency Inversion Principle:依存性逆転の原則

Dependency Inversion Principle(依存性逆転の原則)とは、疎結合に関する原則です。

  • 上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも抽象化に依存すべきである。
  • 抽象化は、詳細に依存してはならない。詳細が抽象化に依存すべきである。

DIコンテナとは

DIコンテナは、DIを実現するための便利なフレームワーク/ライブラリです。
C#ではMicrosoft社製のDIコンテナMicrosoft.Extensions.DependencyInjectionが用意されてます。
DIコンテナはインスタンスのライフサイクル(有効期間)を簡単に管理できるのが利点の1つです。

メソッド 説明
AddTransient DIするごとに、インスタンスを生成する
AddScoped クライアント要求 (接続) ごとに、インスタンスを生成する
AddSingleton アプリケーション内で1つのインスタンスを生成する

対応の流れ

「DI対応前」⇒「DI対応後」⇒「DIコンテナ対応後」の流れをまとめした。

DI対応前

ClassB内でClassCのインスタンスを生成してます。クラス間の依存度が高く密結合です。
イメージ

DI対応後

ClassB内で行っているClassCのインスタンス生成を、コンストラクタ経由でインスタンスを注入するようにします。
Main関数は、アプリで使用するすべてのクラスのインスタンスを生成するようになります。
イメージ

DIコンテナ対応後

ClassA、ClassB、ClassCのメソッドをインターフェイス化します。
Main関数で、各インターフェイスをDIコンテナに登録し、
使用時にDIコンテナから返されるインターフェイスを呼び出すようにします。
イメージ

C#でDIコンテナを使ってDIしてみる

ソースコードはGitHubリポジトリにあります。

環境

  • Windows 10 64bit
  • Visual Studio Community 2022
  • C#
  • .NET 7.0
  • コンソールアプリ

構成

DIコンテナ対応後のイメージ図を基に構成してます。

.sln
├─ .csproj
├─ Program.cs // main関数
├─ InterfaceA.cs
├─ ClassA.cs
├─ InterfaceB.cs
├─ ClassB.cs
├─ InterfaceC.cs
└─ ClassC.cs

プロジェクト作成

プロジェクトテンプレートの「コンソールアプリ」を使用します。
イメージ

デフォルトでProgram.csが作成されます。
イメージ

DIコンテナのインストール

NuGetでMicrosoft.Extensions.DependencyInjectionをインストールします。
イメージ

クラス作成

インターフェイス、クラスを作成します。

InterfaceA.cs

namespace ConsoleApp
{
    internal interface InterfaceA
    {
        void DoAction();
    }
}

ClassA.cs

namespace ConsoleApp
{
    internal class ClassA : InterfaceA
    {
        public void DoAction()
        {
            Console.WriteLine("ClassA");
        }
    }
}

InterfaceB.cs

namespace ConsoleApp
{
    internal interface InterfaceB
    {
        void DoAction();
    }
}

ClassB.cs

namespace ConsoleApp
{
    internal class ClassB : InterfaceB
    {
        private InterfaceC _c;
        public ClassB(InterfaceC c)
        {
            _c = c;
        }

        public void DoAction()
        {
            Console.WriteLine("ClassB");
            _c.DoAction();
        }
    }
}

InterfaceC.cs

namespace ConsoleApp
{
    internal interface InterfaceC
    {
        void DoAction();
    }
}

ClassC.cs

namespace ConsoleApp
{
    internal class ClassC : InterfaceC
    {
        public void DoAction()
        {
            Console.WriteLine("ClassC");
        }
    }
}

DIする

DIコンテナを使ってDIします。
Program.csMain()でDIコンテナにインターフェイスを登録し使用します。

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleApp
{
    internal class Program
    {
        static void Main(string[] args)
        {
            using var serviceProvider = new ServiceCollection()
                .AddTransient<InterfaceA, ClassA>()
                .AddTransient<InterfaceB, ClassB>()
                .AddTransient<InterfaceC, ClassC>()
                .BuildServiceProvider();

            var a = serviceProvider.GetRequiredService<InterfaceA>();
            a.DoAction();

            var b = serviceProvider.GetRequiredService<InterfaceB>();
            b.DoAction();
        }
    }
}

実行

作成したアプリを実行するとコンソールに下記が出力されました。

ClassA
ClassB
ClassC

おわりに

DI自体は簡単に実現できることが分かりました。
ただ、簡単にできるがゆえに複雑に実装してしまう傾向があるようです。
今後は、DIのアンチパターンも学びつつ業務等に活かしたいと思います。

参考