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.