Какие основные подходы (паттерны) внедрения зависимостей (Dependency Injection) используются в 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, так как он скрывает зависимости и усложняет тестирование.

Ответ 18+ 🔞

Давай разберём эту тему про внедрение зависимостей, но без занудства, как есть. Представь, что ты собираешь какую-то хуёвину из деталей, и тебе нужно, чтобы тебе эти детали просто дали, а не ты их сам искал по всему заводу.

1. Внедрение через конструктор — это когда тебе всё суют в руки сразу, как только ты родился.

Самый нормальный, человеческий способ. Ты создал класс — и сразу требуешь: "Мужики, дайте мне валидатор, репозиторий и логгер, а без них я работать не буду, я сяду и буду орать". Объект сразу создаётся готовым к бою, и потом уже никто не может тебе сказать "ой, а логгер-то забыли".

public class OrderProcessor
{
    private readonly IOrderValidator _validator;
    private readonly IOrderRepository _repository;
    private readonly ILogger<OrderProcessor> _logger;

    // Смотри сюда — родился и сразу орёт: дайте три вещи, иначе ArgumentNullException!
    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. Внедрение через метод — это когда тебе нужна какая-то хуйня только один раз, для конкретного дела.

Зачем тащить в свой класс на всю жизнь какую-то тяжелую зависимость, если она нужна только чтобы один раз что-то отформатировать? Передал её в метод — использовал — забыл. Удобно, не захламляет класс.

public class ReportGenerator
{
    // Классу вообще похуй на IReportFormatter, он живёт без него
    public string GenerateReport(ReportData data, IReportFormatter formatter)
    {
        // Пришёл — сделал — ушёл
        return formatter.Format(data);
    }
}

Типа "одолжи на пять минут дрель, я тут одну дырку просверлю". Не покупать же её на всю жизнь.

3. Внедрение через свойство — это как опциональная хуйня, которая может быть, а может и не быть.

Создал объект, а потом, может быть, когда-нибудь, если захочешь — присобачишь ему какую-то функциональность через свойство. Проблема в том, что объект может какое-то время быть в некондиции, с пустым свойством. Поэтому так делают редко, обычно для всяких опциональных плюшек или когда фреймворк сам тебе что-то подсовывает.

public class NotificationService
{
    // Логгер есть? — Используем. Нет? — Ну и хуй с ним, работаем без него.
    public ILogger<NotificationService> Logger { get; set; }

    public void Send(string message)
    {
        Logger?.LogDebug("Sending: {Message}", message);
        // ... основная работа
    }
}

Это как машина без магнитолы. Ехать-то можно, но скучно. Поставил потом — хорошо, не поставил — ну и ладно.

А теперь главное, что нужно запомнить, чтобы не быть мудаком:

  • Используй внедрение через конструктор для всего, без чего твой класс — просто кусок беспомощного говна. Это главный, правильный путь.
  • Работай с интерфейсами, а не с конкретными классами. Иначе ты привариваешь себе намертво колесо от "Запорожца" и потом охуеешь, когда захочешь пересадиться на "Мерседес".
  • Все зависимости регистрируй в контейнере (в том же Program.cs). Это такой главный склад, откуда все будут получать что нужно. Не регистрируешь — получишь InvalidOperationException и будешь сидеть, чесать репу.
  • Не используй Service Locator (антипаттерн). Это когда ты не просишь зависимости явно, а лезешь в какой-то глобальный мешок и сам оттуда что-то выковыриваешь. Это пиздец как неудобно и для тестов, и для понимания, откуда что берётся. Это как вместо "дайте мне молоток" ты бежишь на склад и сам его ищешь, пока все ждут.

Вот и вся философия. Сделал зависимости явными — живи спокойно, тестируй легко, меняй реализации без боли. Сделал через жопу — получи спагетти-код, в котором через месяц сам не разберёшься. Выбирай.