Gymterview
middle

Как Dependency Injection работает как архитектурный принцип?

Dependency Injection (DI, внедрение зависимостей) – принцип, при котором объект не создаёт свои зависимости сам, а получает их извне (через конструктор, сеттер или поле). На архитектурном уровне DI обеспечивает инверсию зависимостей – модули высокого уровня не зависят от модулей низкого уровня.

Пример
Без DI (жёсткая связь):              С DI (слабая связь):

PaymentService                        PaymentService
  └── new JpaPaymentRepo()              └── PaymentRepository (интерфейс)
  └── new SmtpNotifier()                          ↑ внедряется извне
                                        JpaPaymentRepo implements PaymentRepository
Сервис ЗНАЕТ о реализациях.           Сервис знает только интерфейс.
Нельзя подменить для теста.           Легко подменить на мок.

Три способа внедрения в Spring

Пример кода
// 1. Через конструктор (РЕКОМЕНДУЕТСЯ)
@Service
public class PaymentService {
    private final PaymentRepository repository;
    private final NotificationPort notifier;

    // Spring внедрит реализации автоматически
    public PaymentService(PaymentRepository repository, NotificationPort notifier) {
        this.repository = repository;
        this.notifier = notifier;
    }
}

// 2. Через сеттер
@Service
public class PaymentService {
    private PaymentRepository repository;

    @Autowired
    public void setRepository(PaymentRepository repository) {
        this.repository = repository;
    }
}

// 3. Через поле (НЕ рекомендуется)
@Service
public class PaymentService {
    @Autowired
    private PaymentRepository repository;
}

Почему конструктор предпочтителен

  • Зависимости явно видны в сигнатуре конструктора.
  • Объект неизменяем (final-поля).
  • Не может быть создан без зависимостей (нет NullPointerException в рантайме).
  • Легко тестировать без Spring-контекста – просто передать моки в конструктор.

DI на архитектурном уровне

DI – это механизм реализации Dependency Inversion Principle (DIP). На уровне архитектуры это означает:

  • Модуль бизнес-логики определяет интерфейсы (порты), которые ему нужны.
  • Модуль инфраструктуры предоставляет реализации (адаптеры).
  • Конфигурация (Composition Root) связывает всё вместе.
Пример
┌──────────────────────────────┐
│   domain (ядро)              │
│   ├── PaymentService         │
│   ├── PaymentRepository      │  ← интерфейс
│   └── NotificationPort       │  ← интерфейс
└──────────────────────────────┘

┌──────────────────────────────┐
│   infrastructure             │
│   ├── JpaPaymentRepository   │  → implements PaymentRepository
│   └── KafkaNotificationAdapter│ → implements NotificationPort
└──────────────────────────────┘

┌──────────────────────────────┐
│   config (Composition Root)  │
│   └── @Configuration класс   │  ← связывает интерфейсы с реализациями
└──────────────────────────────┘

Преимущества DI как архитектурного принципа

  • Модули можно разрабатывать и тестировать независимо.
  • Легко подменять реализации (PostgreSQL -> MongoDB, SMTP -> Kafka).
  • Обеспечивает тестируемость через мок-объекты.
  • Снижает coupling между модулями.
  • В гексагональной архитектуре DI – ключевой механизм связи портов с адаптерами.

На собеседовании: Интервьюер хочет услышать связь DI с DIP (SOLID) и понимание архитектурной роли, а не только механику Spring. Частая ошибка – описывать только @Autowired, не упоминая инверсию зависимостей и Composition Root.