Внедрение зависимостей (Dependency Injection) в C#

Dependency Injection (DI) — это паттерн проектирования, который позволяет передавать зависимости в класс извне, а не создавать их внутри. Это способствует слабой связанности и улучшает тестируемость кода.

Основные принципы DI

  1. Инверсия управления (IoC) — принцип, согласно которому управление зависимостями передается из объекта во внешнюю систему. Это уменьшает связность компонентов и делает код более модульным.
  2. Разделение интерфейсов и реализации — код работает с абстракциями, а не с конкретными реализациями.
  3. Контейнеры DI — инструменты для автоматического управления зависимостями.

Реализация Dependency Injection в C#

Внедрение через конструктор

Наиболее распространенный способ внедрения зависимостей:

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

public class Service
{
    private readonly ILogger _logger;
    
    public Service(ILogger logger)
    {
        _logger = logger;
    }
    
    public void Execute()
    {
        _logger.Log("Executing service...");
    }
}

Использование DI-контейнера в .NET

В .NET встроен контейнер для управления зависимостями. Рассмотрим его использование в ASP.NET Core.

Регистрация зависимостей

В файле Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<ILogger, ConsoleLogger>();
builder.Services.AddTransient<Service>();

var app = builder.Build();

Разрешение зависимостей

Внедрение в контроллер или сервис:

public class HomeController
{
    private readonly Service _service;
    
    public HomeController(Service service)
    {
        _service = service;
    }
    
    public void Run()
    {
        _service.Execute();
    }
}

Жизненный цикл зависимостей

При регистрации зависимостей в контейнере DI необходимо учитывать их жизненный цикл. В .NET предусмотрены три основных типа:

  1. Transient — создается новый экземпляр объекта при каждом запросе.
    builder.Services.AddTransient<IMyService, MyService>();
  2. Scoped — один экземпляр объекта используется в пределах одного запроса (например, в ASP.NET Core).
    builder.Services.AddScoped<IMyService, MyService>();
  3. Singleton — один экземпляр объекта используется в течение всего времени работы приложения.
    builder.Services.AddSingleton<IMyService, MyService>();

Выбор подходящего типа зависит от контекста использования зависимостей.

Преимущества Dependency Injection

  • Упрощает тестирование с помощью подменяемых зависимостей (Mock-объектов).
  • Улучшает поддержку и расширяемость кода.
  • Способствует принципам SOLID, особенно SRP и DIP.