Dependency Injection (впровадження залежностей) — один з фундаментальних патернів проектування в сучасній Java-розробці. Цей принцип допомагає створювати гнучкі, тестовані та підтримувані додатки. Давайте розберемося, що це таке і як правильно його застосовувати.
Що таке Dependency Injection?
Dependency Injection — це техніка, при якій об’єкт отримує свої залежності ззовні, а не створює їх самостійно. Замість того, щоб клас сам інстанціював потрібні йому об’єкти, вони передаються йому через конструктор, setter-методи або поля.
Розглянемо проблему на простому прикладі. Припустимо, у нас є сервіс для відправки сповіщень:
public class NotificationService {
private EmailSender emailSender;
public NotificationService() {
this.emailSender = new EmailSender(); // Жорсткий зв'язок!
}
public void notifyUser(String message) {
emailSender.send(message);
}
}JavaУ цьому коді NotificationService жорстко пов’язаний з конкретною реалізацією EmailSender. Якщо ми захочемо використовувати SMS-сповіщення або тестувати клас з mock-об’єктом, нам доведеться змінювати код самого сервісу.
Три способи впровадження залежностей
1. Впровадження через конструктор
Найбільш бажаний спосіб, що забезпечує незмінність залежностей:
public class NotificationService {
private final MessageSender messageSender;
public NotificationService(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void notifyUser(String message) {
messageSender.send(message);
}
}JavaПереваги: об’єкт завжди знаходиться у валідному стані, залежності явно видимі, легко тестувати.
2. Впровадження через setter-методи
Корисно для опціональних залежностей:
public class NotificationService {
private MessageSender messageSender;
public void setMessageSender(MessageSender messageSender) {
this.messageSender = messageSender;
}
}Java3. Впровадження через поля
Використовується у фреймворках, але вимагає reflection:
public class NotificationService {
@Inject
private MessageSender messageSender;
}JavaDI-контейнери та фреймворки
Вручну створювати та зв’язувати об’єкти у великих додатках непрактично. Для цього існують DI-контейнери.
Spring Framework
Spring — найпопулярніший DI-фреймворк в Java-екосистемі:
@Component
public class NotificationService {
private final MessageSender messageSender;
@Autowired
public NotificationService(MessageSender messageSender) {
this.messageSender = messageSender;
}
}
@Component
public class EmailSender implements MessageSender {
@Override
public void send(String message) {
// Логіка відправки email
}
}JavaSpring автоматично виявить компоненти, створить їх екземпляри та впровадить залежності.
Google Guice
Більш легкий альтернативний фреймворк:
public class NotificationModule extends AbstractModule {
@Override
protected void configure() {
bind(MessageSender.class).to(EmailSender.class);
}
}
public class NotificationService {
@Inject
private MessageSender messageSender;
}JavaПереваги Dependency Injection
Тестованість. Можна легко підставити mock-об’єкти при тестуванні:
@Test
public void testNotifyUser() {
MessageSender mockSender = mock(MessageSender.class);
NotificationService service = new NotificationService(mockSender);
service.notifyUser("Test message");
verify(mockSender).send("Test message");
}JavaГнучкість. Легко змінювати реалізації без зміни клієнтського коду. Можна переключитися з email на SMS, просто змінивши конфігурацію.
Слабке зв’язування. Класи залежать від абстракцій (інтерфейсів), а не від конкретних реалізацій, що відповідає принципу інверсії залежностей (DIP) з SOLID.
Повторне використання коду. Один і той же клас можна використовувати з різними залежностями в різних контекстах.
Поширені помилки
Уникайте circular dependencies (циклічних залежностей), коли клас A залежить від класу B, а B залежить від A. Більшість DI-контейнерів виявлять це на етапі ініціалізації.
Не зловживайте впровадженням залежностей для простих value objects або утилітарних класів без стану.
Намагайтеся використовувати constructor injection замість field injection для обов’язкових залежностей — це робить залежності явними та спрощує тестування.
Оновлення для Spring Framework
Spring Framework з сучасними підходами:
- Constructor Injection без @Autowired – починаючи зі Spring 4.3 анотація не потрібна для одного конструктора
- Використання Lombok – для зменшення boilerplate-коду
- Умовне впровадження – @ConditionalOnProperty для гнучкої конфігурації
- Множинні реалізації – як працювати з List<Interface> та Map
- @Primary та @Qualifier – для вибору конкретних реалізацій
- Scopes – singleton, prototype, request, session
- Java-based Configuration – сучасна конфігурація без XML
Ці приклади показують реальні use-case’и, з якими стикаються розробники у production-додатках.
Висновок
Dependency Injection — потужний інструмент для створення якісного Java-коду. Він допомагає дотримуватися принципів SOLID, робить код більш тестованим та підтримуваним. Сучасні фреймворки на кшталт Spring значно спрощують роботу з DI, беручи на себе рутинні задачі зі створення та зв’язування об’єктів.
Почніть застосовувати DI у своїх проектах поступово: спочатку освойте впровадження через конструктор, потім вивчіть можливості обраного фреймворку. З часом цей підхід стане природною частиною вашого стилю програмування.
Spring Framework приклад
## DI-контейнери та фреймворки
Вручну створювати та зв'язувати об'єкти у великих додатках непрактично. Для цього існують DI-контейнери.
### Spring Framework: Сучасні Підходи
Spring — найпопулярніший DI-фреймворк в Java-екосистемі. Розглянемо сучасні практики роботи з ним.
#### Constructor Injection без @Autowired
Починаючи зі Spring 4.3, якщо у класу є лише один конструктор, анотація `@Autowired` не потрібна:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final NotificationService notificationService;
// @Autowired не потрібен!
public OrderService(OrderRepository orderRepository,
PaymentService paymentService,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
public Order createOrder(OrderRequest request) {
Order order = orderRepository.save(request.toOrder());
paymentService.processPayment(order);
notificationService.notifyUser(order);
return order;
}
}
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final PriceCalculator priceCalculator;
private final StockService stockService;
public Product createProduct(ProductDto dto) {
Product product = dto.toEntity();
product.setPrice(priceCalculator.calculate(product));
stockService.reserve(product);
return productRepository.save(product);
}
}
Анотація `@RequiredArgsConstructor` з Lombok автоматично генерує конструктор для всіх `final` полів.
#### Умовне впровадження залежностей
Spring дозволяє гнучко керувати тим, які beans та створювати залежно від умов:
@Configuration
public class MessagingConfig {
@Bean
@ConditionalOnProperty(name = "messaging.type", havingValue = "email")
public MessageSender emailSender() {
return new EmailSender();
}
@Bean
@ConditionalOnProperty(name = "messaging.type", havingValue = "sms")
public MessageSender smsSender() {
return new SmsSender();
}
@Bean
@ConditionalOnMissingBean
public MessageSender defaultSender() {
return new EmailSender();
}
}
#### Робота з множинними реалізаціями
Коли є декілька реалізацій одного інтерфейсу:
public interface PaymentProcessor {
void process(Payment payment);
PaymentType supportedType();
}
@Component
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void process(Payment payment) {
// Обробка кредитної картки
}
@Override
public PaymentType supportedType() {
return PaymentType.CREDIT_CARD;
}
}
@Component
public class PayPalProcessor implements PaymentProcessor {
@Override
public void process(Payment payment) {
// Обробка PayPal
}
@Override
public PaymentType supportedType() {
return PaymentType.PAYPAL;
}
}
@Service
public class PaymentService {
private final Map<PaymentType, PaymentProcessor> processors;
public PaymentService(List<PaymentProcessor> processorList) {
this.processors = processorList.stream()
.collect(Collectors.toMap(
PaymentProcessor::supportedType,
Function.identity()
));
}
public void processPayment(Payment payment) {
PaymentProcessor processor = processors.get(payment.getType());
if (processor == null) {
throw new UnsupportedPaymentTypeException(payment.getType());
}
processor.process(payment);
}
}
#### @Primary та @Qualifier
Для вибору конкретної реалізації:
@Component
@Primary
public class FastEmailSender implements MessageSender {
// Буде використовуватися за замовчуванням
}
@Component
@Qualifier("reliable")
public class ReliableEmailSender implements MessageSender {
// Треба явно вказувати @Qualifier
}
@Service
public class CriticalNotificationService {
private final MessageSender messageSender;
public CriticalNotificationService(@Qualifier("reliable") MessageSender messageSender) {
this.messageSender = messageSender;
}
}
#### Scopes у Spring
Spring підтримує різні області видимості beans:
@Component
@Scope("singleton") // За замовчуванням
public class ConfigurationCache {
// Один екземпляр на весь додаток
}
@Component
@Scope("prototype")
public class RequestHandler {
// Новий екземпляр при кожному запиті
}
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
// Один екземпляр на HTTP-запит
}
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession {
// Один екземпляр на HTTP-сесію
}
#### Java-based Configuration
Сучасний підхід до конфігурації без XML:
@Configuration
@ComponentScan(basePackages = "com.example.app")
@EnableTransactionManagement
public class AppConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("password");
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
@Profile("production")
public PasswordEncoder productionPasswordEncoder() {
return new BCryptPasswordEncoder(12);
}
@Bean
@Profile("dev")
public PasswordEncoder devPasswordEncoder() {
return new BCryptPasswordEncoder(4);
}
}Java




