Какие основные подходы (паттерны) внедрения зависимостей (Dependency Injection) используются в C#?

«Какие основные подходы (паттерны) внедрения зависимостей (Dependency Injection) используются в C#?» — вопрос из категории Архитектура, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Внедрение зависимостей (DI) в C# реализуется через три основных паттерна, которые определяют, как зависимость передается в потребительский класс:

  1. Внедрение через конструктор (Constructor Injection)

    • Самый предпочтительный и распространенный подход.
    • Зависимости объявляются как readonly поля и передаются через параметры конструктора.
    • Гарантирует, что объект будет создан в валидном состоянии (все зависимости предоставлены).

      public class OrderProcessor
      {
      private readonly IOrderValidator _validator;
      private readonly IOrderRepository _repository;
      private readonly ILogger<OrderProcessor> _logger;
      
      // Зависимости явно требуются при создании
      public OrderProcessor(
          IOrderValidator validator,
          IOrderRepository repository,
          ILogger<OrderProcessor> logger)
      {
          _validator = validator ?? throw new ArgumentNullException(nameof(validator));
          _repository = repository ?? throw new ArgumentNullException(nameof(repository));
          _logger = logger ?? throw new ArgumentNullException(nameof(logger));
      }
      
      public void Process(Order order)
      {
          _validator.Validate(order);
          _repository.Save(order);
          _logger.LogInformation("Order {OrderId} processed.", order.Id);
      }
      }
  2. Внедрение через метод (Method Injection)

    • Зависимость передается в качестве параметра конкретного метода, которому она нужна.
    • Полезно, когда зависимость требуется только для одного действия и не должна храниться в состоянии класса.
      public class ReportGenerator
      {
      // IReportFormatter не является частью состояния класса
      public string GenerateReport(ReportData data, IReportFormatter formatter)
      {
          return formatter.Format(data);
      }
      }
  3. Внедрение через свойство (Property Injection / Setter Injection)

    • Зависимость устанавливается через публичное свойство после создания объекта.
    • Используется реже, так как объект может какое-то время находиться в невалидном состоянии (свойство null). Часто применяется в фреймворках (например, для внедрения в контроллеры MVC) или для опциональных зависимостей.

      public class NotificationService
      {
      // Опциональный логгер. Сервис может работать и без него.
      public ILogger<NotificationService> Logger { get; set; }
      
      public void Send(string message)
      {
          Logger?.LogDebug("Sending: {Message}", message);
          // ... логика отправки
      }
      }

Ключевые практики:

  • Всегда предпочитайте Constructor Injection для обязательных зависимостей.
  • Программируйте на уровне интерфейсов, а не конкретных реализаций.
  • Регистрируйте зависимости в DI-контейнере (например, в Startup.cs или Program.cs ASP.NET Core).
  • Избегайте антипаттерна Service Locator, так как он скрывает зависимости и усложняет тестирование.