はじめに
ソフトウェア設計を学び始めると、「依存性の逆転」「Dependency Injection(DI)」「DIコンテナ」などの用語に出会います。
実際、私もプロジェクトの大幅改修に際して、依存性注入を改善する機会がありました。
これらは、柔軟で保守性の高いコードを書くために重要な概念です。
この記事では、C#を例に、Dependency Injectionの基本から、サービスロケーターやDIコンテナについて学んだことを備忘録としてまとめていまs
初学者ですので、間違いなどございましたらコメントでご指摘いただけますと幸いです。
Dependency Injection(DI)とは?
Dependency Injection(依存性の注入)とは、オブジェクトが依存する他のオブジェクトを外部から提供(注入)してもらう設計パターンです。これにより、オブジェクト同士の結合度を下げ、柔軟でテストしやすいコードを実現できます。
例:依存性の注入を行わない場合
以下のFooService
クラスは、Logger
クラスに直接依存しています。
public class Logger
{
public void Write(string message)
{
Console.WriteLine("Message: " + message);
}
}
public class FooService
{
private readonly Logger _logger = new();
public void Execute()
{
_logger.Write("Foo!");
}
}
この実装では、FooService
がLogger
を直接生成しているため、以下の問題があります:
-
柔軟性の欠如:
Logger
の実装を変更するには、FooService
のコードも変更する必要があります。 -
テストの困難さ:
Logger
をモックに差し替えることができないため、FooService
の単体テストが難しくなります。
例:依存性の注入を行う場合
依存性の注入を行うことで、上記の問題を解決できます。
public interface ILogger
{
void Write(string message);
}
public class Logger : ILogger
{
public void Write(string message)
{
Console.WriteLine("Message: " + message);
}
}
public class FooService
{
private readonly ILogger _logger;
public FooService(ILogger logger)
{
_logger = logger;
}
public void Execute()
{
_logger.Write("Foo!");
}
}
この実装では、FooService
はILogger
インターフェースに依存しており、具体的な実装は外部から注入されます。これにより、Logger
の実装を変更しても、FooService
のコードを変更する必要がなくなります。また、テスト時にはモックのILogger
を注入することで、FooService
の単体テストが容易になります。
依存性の注入の実現方法
手動での注入
小規模なアプリケーションでは、手動で依存性を注入することが可能です。
var logger = new Logger();
var fooService = new FooService(logger);
fooService.Execute();
しかし、アプリケーションが大規模になると、依存関係の管理が複雑になり、手動での注入が困難になります。
サービスロケーター
サービスロケーターは、依存性を解決するための仕組みの一つです。以下は、簡易的なサービスロケーターの実装例です:
using System;
using System.Collections.Generic;
public static class ServiceLocator
{
private static readonly Dictionary<Type, object> _services = new();
public static void Register<T>(T service)
{
_services[typeof(T)] = service;
}
public static T Resolve<T>()
{
return (T)_services[typeof(T)];
}
}
使用例:
ServiceLocator.Register<ILogger>(new Logger());
var fooService = new FooService(ServiceLocator.Resolve<ILogger>());
fooService.Execute();
サービスロケーターは便利ですが、使用しすぎるとコードの可読性やテストのしやすさが低下する可能性があります。そのため、適切な場面での使用が求められます。
DIコンテナ
DIコンテナは、依存性の注入を自動化するためのフレームワークです。C#では、Microsoft.Extensions.DependencyInjection
などのライブラリが利用できます。
DIコンテナを使用することで、以下の利点があります:
- 依存関係の自動解決:コンストラクタの引数に応じて、必要な依存性を自動的に注入します。
- ライフサイクルの管理:シングルトンやスコープなど、オブジェクトのライフサイクルを管理できます。
- モジュール化:依存性の登録をモジュール単位で管理できます。
依存性の注入のタイミング
依存性の注入は、主に以下の2つのタイミングで行われます:
-
アプリケーションの起動時:DIコンテナにサービスを登録し、必要な依存性を解決する際に注入されます。これは一般的にもっとも多いパターンで、ASP.NET Core などのフレームワークでは、
Program.cs
やStartup.cs
などで設定されます。 - オブジェクトの生成時:手動で依存性を注入する場合、対象のオブジェクトを生成するタイミングで注入されます。この方法は、小規模なアプリケーションやテストコードなどで使用されます。
依存性注入のライフサイクル
C#での依存性注入(DI)では、3つの依存性注入メソッドのライフサイクル(AddTransient、AddScoped、AddSingleton)が使用できます。
ざっくりまとめると以下のようになっています。
ライフタイム インスタンスの生成タイミング
- AddTransient 要求ごとに新しいインスタンスを生成
- AddScoped リクエストごとに1つのインスタンスを生成
-
AddSingleton アプリケーション全体で1つのインスタンスを生成
それぞれ依存性注入するのは変わりませんが、オブジェクトの生成タイミングが違うので実際に比較します。
適切なタイミングで依存性を注入することで、アプリケーションの初期化処理を明確にし、
不要な依存を回避する設計が可能になります。