Dependency Injection(依存性注入)
Dependency Injection(依存性注入)とは
Dependency Injection(DI)は、クラスが必要とする依存関係を外部から注入するデザインパターンです。これにより、クラス間の結合度を下げ、テストしやすく、保守性の高いコードを書くことができます。
DIを使わない場合の問題点
public class OrderService
{
private readonly EmailService _emailService;
public OrderService()
{
// クラス内部で依存関係を直接生成
_emailService = new EmailService();
}
public void ProcessOrder(Order order)
{
// 注文処理
_emailService.SendConfirmation(order);
}
}
問題点:
OrderServiceがEmailServiceに強く依存している- テスト時にモックに置き換えることが困難
EmailServiceの変更がOrderServiceに影響する- 柔軟性が低い
DIを使った場合
public interface IEmailService
{
void SendConfirmation(Order order);
}
public class EmailService : IEmailService
{
public void SendConfirmation(Order order)
{
// メール送信処理
}
}
public class OrderService
{
private readonly IEmailService _emailService;
// コンストラクタで依存関係を注入
public OrderService(IEmailService emailService)
{
_emailService = emailService;
}
public void ProcessOrder(Order order)
{
// 注文処理
_emailService.SendConfirmation(order);
}
}
メリット:
- インターフェースを使用することで結合度が下がる
- テスト時にモック実装を注入できる
- 実装の切り替えが容易
- 依存関係が明確になる
DIコンテナとは
DIコンテナ(Dependency Injection Container)は、依存関係の登録、解決、ライフタイム管理を自動的に行うフレームワークです。IoC(Inversion of Control:制御の反転)コンテナとも呼ばれます。
DIコンテナの役割
DIコンテナは主に以下の3つの役割を担います:
1. サービスの登録(Registration)
アプリケーション起動時に、インターフェースと実装クラスの対応関係をコンテナに登録します。
// DIコンテナにサービスを登録
var services = new ServiceCollection();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IOrderRepository, OrderRepository>();
2. 依存関係の解決(Resolution)
必要な時にコンテナからサービスのインスタンスを取得します。コンテナは依存関係を自動的に解決し、必要なオブジェクトをすべて生成・注入します。
// コンテナからサービスを取得
var serviceProvider = services.BuildServiceProvider();
var orderService = serviceProvider.GetRequiredService<OrderService>();
// OrderServiceが必要とするIEmailService、IOrderRepositoryも自動的に注入される
3. ライフタイム管理(Lifetime Management)
サービスの生成と破棄のタイミングを管理します(Transient、Scoped、Singleton)。
.NETのDIコンテナ
.NET Coreには標準でDIコンテナが組み込まれており、Microsoft.Extensions.DependencyInjection名前空間で提供されています。
// ASP.NET Coreアプリケーションの場合
var builder = WebApplication.CreateBuilder(args);
// builder.Services がIServiceCollection(DIコンテナ)
builder.Services.AddScoped<IEmailService, EmailService>();
var app = builder.Build();
// app.Services がIServiceProvider(サービスの取得に使用)
DIコンテナの動作原理
DIコンテナはコンストラクタインジェクションを使用して、依存関係を自動的に解決します。
// 1. サービスの登録
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IPaymentService, PaymentService>();
services.AddScoped<OrderService>();
// 2. OrderServiceの定義
public class OrderService
{
public OrderService(IEmailService emailService, IPaymentService paymentService)
{
// コンストラクタで依存関係を受け取る
}
}
// 3. DIコンテナによる解決プロセス
// OrderServiceが要求される
// ↓
// コンテナがOrderServiceのコンストラクタを確認
// ↓
// IEmailServiceとIPaymentServiceが必要だと認識
// ↓
// EmailServiceとPaymentServiceのインスタンスを作成
// ↓
// それらをOrderServiceのコンストラクタに渡す
// ↓
// OrderServiceのインスタンスが返される
DIコンテナを使わない手動DI
DIコンテナを使わずに手動で依存関係を注入することも可能ですが、複雑なアプリケーションでは管理が困難になります。
// 手動でのDI(DIコンテナなし)
var dbContext = new ApplicationDbContext(options);
var emailService = new EmailService(emailSettings);
var paymentService = new PaymentService(paymentSettings);
var orderRepository = new OrderRepository(dbContext);
var orderService = new OrderService(orderRepository, emailService, paymentService);
// 問題点:
// - 依存関係が増えるとコードが複雑になる
// - ライフタイム管理を手動で行う必要がある
// - 依存関係の変更時に多くの箇所を修正する必要がある
DIコンテナのメリット
- 自動的な依存関係解決: コンストラクタを見て必要なサービスを自動的に注入
- ライフタイム管理の自動化: オブジェクトの生成と破棄を自動管理
- 設定の一元化: すべてのサービス登録を一箇所で管理
- 保守性の向上: 依存関係の変更が容易
- テスト容易性: モックやスタブへの置き換えが簡単
サービスロケーターパターンとの違い
DIコンテナと混同されやすいパターンにサービスロケーターがありますが、これはアンチパターンとされています。
// ❌ サービスロケーターパターン(アンチパターン)
public class OrderService
{
public void ProcessOrder(Order order)
{
// クラス内部でコンテナから直接取得
var emailService = ServiceLocator.GetService<IEmailService>();
emailService.SendConfirmation(order);
}
}
// ✅ DIコンテナを使った正しいパターン
public class OrderService
{
private readonly IEmailService _emailService;
// コンストラクタで注入
public OrderService(IEmailService emailService)
{
_emailService = emailService;
}
public void ProcessOrder(Order order)
{
_emailService.SendConfirmation(order);
}
}
サービスロケーターが避けられる理由:
- 依存関係が隠蔽され、クラスの依存関係が不明確
- テストが困難(依存関係がコンストラクタに現れない)
- 実行時エラーが発生しやすい
Service Collectionとは
.NET Coreでは、IServiceCollectionを使用してDIコンテナにサービスを登録します。これにより、アプリケーション全体で依存関係を管理できます。
サービスの登録
ASP.NET Coreアプリケーションでは、通常Program.csまたはStartup.csでサービスを登録します。
var builder = WebApplication.CreateBuilder(args);
// サービスの登録
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<OrderService>();
var app = builder.Build();
サービスのライフタイム
DIコンテナでは、3種類のライフタイムを指定できます:
1. Transient(一時的)
builder.Services.AddTransient<IMyService, MyService>();
- リクエストされるたびに新しいインスタンスを作成
- 軽量でステートレスなサービスに適している
- メモリ使用量が多くなる可能性がある
使用例: ユーティリティクラス、軽量なヘルパークラス
2. Scoped(スコープ付き)
builder.Services.AddScoped<IMyService, MyService>();
- HTTPリクエストごとに1つのインスタンスを作成
- 同じリクエスト内では同じインスタンスが使用される
- 最も一般的に使用される
使用例: データベースコンテキスト(Entity Framework Core)、リポジトリ、ビジネスロジック
3. Singleton(シングルトン)
builder.Services.AddSingleton<IMyService, MyService>();
- アプリケーション起動時に1つのインスタンスを作成
- アプリケーション全体で同じインスタンスが使用される
- スレッドセーフである必要がある
使用例: 設定オブジェクト、キャッシュ、ロガー
ライフタイムの選択ガイド
| ライフタイム | いつ使うか | 注意点 |
|---|---|---|
| Transient | 軽量でステートレスなサービス | 多くのインスタンスが作成されるため、重いオブジェクトには不向き |
| Scoped | リクエストごとに状態を保持するサービス | Web APIやMVCアプリケーションで最も一般的 |
| Singleton | アプリケーション全体で共有するサービス | スレッドセーフでなければならない |
実践例
1. 基本的なサービス登録と使用
// インターフェース定義
public interface IProductRepository
{
Task<Product> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
}
// 実装
public class ProductRepository : IProductRepository
{
private readonly ApplicationDbContext _context;
public ProductRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<Product> GetByIdAsync(int id)
{
return await _context.Products.FindAsync(id);
}
public async Task<IEnumerable<Product>> GetAllAsync()
{
return await _context.Products.ToListAsync();
}
}
// サービス登録(Program.cs)
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IProductRepository, ProductRepository>();
// コントローラーでの使用
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
}
2. 複数の依存関係の注入
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IEmailService _emailService;
private readonly IPaymentService _paymentService;
private readonly ILogger<OrderService> _logger;
// 複数の依存関係をコンストラクタで注入
public OrderService(
IOrderRepository orderRepository,
IEmailService emailService,
IPaymentService paymentService,
ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_emailService = emailService;
_paymentService = paymentService;
_logger = logger;
}
public async Task<bool> ProcessOrderAsync(Order order)
{
try
{
_logger.LogInformation($"Processing order {order.Id}");
var paymentSuccess = await _paymentService.ProcessPaymentAsync(order);
if (!paymentSuccess)
{
_logger.LogWarning($"Payment failed for order {order.Id}");
return false;
}
await _orderRepository.SaveAsync(order);
await _emailService.SendConfirmationAsync(order);
_logger.LogInformation($"Order {order.Id} processed successfully");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing order {order.Id}");
throw;
}
}
}
// サービス登録
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<IPaymentService, PaymentService>();
builder.Services.AddScoped<OrderService>();
3. 設定オブジェクトの注入
// appsettings.json
{
"EmailSettings": {
"SmtpServer": "smtp.example.com",
"Port": 587,
"SenderEmail": "noreply@example.com"
}
}
// 設定クラス
public class EmailSettings
{
public string SmtpServer { get; set; }
public int Port { get; set; }
public string SenderEmail { get; set; }
}
// サービス登録
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection("EmailSettings"));
// 使用例
public class EmailService : IEmailService
{
private readonly EmailSettings _settings;
public EmailService(IOptions<EmailSettings> options)
{
_settings = options.Value;
}
public async Task SendEmailAsync(string to, string subject, string body)
{
// _settings.SmtpServer などを使用してメール送信
}
}
C# 12 Primary Constructorsによる簡略化
C# 12で導入された**Primary Constructors(プライマリコンストラクタ)**を使用すると、DIの記述を大幅に簡略化できます。
従来の書き方
これまでのC#では、依存関係を受け取るためにプライベートフィールドの宣言とコンストラクタでの代入が必要でした。
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IEmailService _emailService;
public OrderService(IOrderRepository orderRepository, IEmailService emailService)
{
_orderRepository = orderRepository;
_emailService = emailService;
}
public void Process()
{
_orderRepository.Save();
_emailService.Send();
}
}
Primary Constructorsを使った書き方
クラス名の直後に引数を定義することで、フィールドの宣言とコンストラクタの定義を省略できます。
// クラス定義で直接依存関係を受け取る
public class OrderService(IOrderRepository orderRepository, IEmailService emailService)
{
public void Process()
{
// 引数をそのままフィールドのように使用可能
orderRepository.Save();
emailService.Send();
}
}
メリット:
- ボイラープレートコード(フィールド宣言、コンストラクタ、代入処理)を削減できる
- コードが読みやすくなる
- 依存関係がクラス定義のトップに明示される
注意点:
- Primary Constructorの引数は、クラス内のどこからでもアクセス可能ですが、明示的に
readonlyフィールドとして定義されるわけではありません(キャプチャされた場合は実質的にフィールドのように振る舞います)。 - 従来の
_プレフィックス付きフィールドを使用したい場合は、明示的にフィールドを定義して初期化することも可能です。
public class OrderService(IOrderRepository orderRepository)
{
// プライベートフィールドに代入することも可能
private readonly IOrderRepository _orderRepository = orderRepository;
}
テストでのDI活用
DIを使用することで、ユニットテストが容易になります。
public class OrderServiceTests
{
[Fact]
public async Task ProcessOrder_Success_ReturnsTrue()
{
// Arrange - モックの準備
var mockOrderRepo = new Mock<IOrderRepository>();
var mockEmailService = new Mock<IEmailService>();
var mockPaymentService = new Mock<IPaymentService>();
var mockLogger = new Mock<ILogger<OrderService>>();
mockPaymentService
.Setup(x => x.ProcessPaymentAsync(It.IsAny<Order>()))
.ReturnsAsync(true);
var service = new OrderService(
mockOrderRepo.Object,
mockEmailService.Object,
mockPaymentService.Object,
mockLogger.Object);
var order = new Order { Id = 1, Total = 100 };
// Act
var result = await service.ProcessOrderAsync(order);
// Assert
Assert.True(result);
mockPaymentService.Verify(x => x.ProcessPaymentAsync(order), Times.Once);
mockEmailService.Verify(x => x.SendConfirmationAsync(order), Times.Once);
}
}
ベストプラクティス
1. インターフェースを使用する
具象クラスではなくインターフェースに依存することで、柔軟性とテスト容易性が向上します。
// ✅ 推奨
builder.Services.AddScoped<IEmailService, EmailService>();
// ❌ 非推奨
builder.Services.AddScoped<EmailService>();
2. 適切なライフタイムを選択する
サービスの性質に応じて適切なライフタイムを選択してください。
// データベースコンテキスト - Scoped
builder.Services.AddDbContext<AppDbContext>();
// ロガー - Singleton
builder.Services.AddSingleton<ILogger, FileLogger>();
// ユーティリティクラス - Transient
builder.Services.AddTransient<IDateTimeProvider, DateTimeProvider>();
3. コンストラクタ注入を優先する
フィールド注入やプロパティ注入ではなく、コンストラクタ注入を使用してください。
// ✅ 推奨 - コンストラクタ注入
public class MyService
{
private readonly IDependency _dependency;
public MyService(IDependency dependency)
{
_dependency = dependency ?? throw new ArgumentNullException(nameof(dependency));
}
}
4. Singleton内でScopedサービスを使用しない
ライフタイムの長いサービスは、短いサービスを直接注入してはいけません。
// ❌ 避けるべき
public class MySingletonService
{
private readonly IScopedService _scopedService; // 問題!
public MySingletonService(IScopedService scopedService)
{
_scopedService = scopedService;
}
}
// ✅ 代替案 - IServiceScopeFactoryを使用
public class MySingletonService
{
private readonly IServiceScopeFactory _scopeFactory;
public MySingletonService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task DoWorkAsync()
{
using var scope = _scopeFactory.CreateScope();
var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
await scopedService.DoSomethingAsync();
}
}
まとめ
- **Dependency Injection(DI)**は、クラスの依存関係を外部から注入するデザインパターン
- DIコンテナは、サービスの登録・解決・ライフタイム管理を自動的に行うフレームワーク
- .NETには標準でDIコンテナが組み込まれている(Microsoft.Extensions.DependencyInjection)
- Service Collectionを使用してDIコンテナにサービスを登録
- 3つのライフタイム: Transient(毎回新規)、Scoped(リクエスト毎)、Singleton(1つのみ)
- インターフェースを使用することでテスト容易性と柔軟性が向上
- 適切なライフタイムの選択とコンストラクタ注入の使用が重要
DIとDIコンテナを正しく使用することで、保守性が高く、テストしやすい、拡張可能なアプリケーションを構築できます。