Dependency Injection в Java

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;
    }
}
Java

3. Впровадження через поля

Використовується у фреймворках, але вимагає reflection:

public class NotificationService {
    @Inject
    private MessageSender messageSender;
}
Java

DI-контейнери та фреймворки

Вручну створювати та зв’язувати об’єкти у великих додатках непрактично. Для цього існують 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
    }
}
Java

Spring автоматично виявить компоненти, створить їх екземпляри та впровадить залежності.

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 з сучасними підходами:

  1. Constructor Injection без @Autowired – починаючи зі Spring 4.3 анотація не потрібна для одного конструктора
  2. Використання Lombok – для зменшення boilerplate-коду
  3. Умовне впровадження – @ConditionalOnProperty для гнучкої конфігурації
  4. Множинні реалізації – як працювати з List<Interface> та Map
  5. @Primary та @Qualifier – для вибору конкретних реалізацій
  6. Scopes – singleton, prototype, request, session
  7. 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

javaadmin

Супер крутий Dev

Related Posts

Dependency Injection у Spring: пояснюємо на пальцях

Коли програміст-початківець вперше відкриває документацію до Spring Framework, на нього одразу висипається купа грізних термінів: Dependency Injection (DI), Inversion of Control (IoC), ApplicationContext, Bean. Здається, що це вища математика. Ми вже розглядали DI в минулих матеріалах але цю дуже не просту тему краще розібрати по болтиках для повного розуміння. Насправді за цими розумними словами ховається надзвичайно проста та красива ідея. Це історія про те, як перестати збирати складні речі вручну й довірити…

Міграція Java-додатків на Spring Boot 3: Spring Boot Properties Migrator

Перехід на Spring Boot 3 є значним кроком для будь-якого Java-додатку, оскільки ця версія вносить багато змін, включаючи перехід на Jakarta, оновлення залежностей та зміни в конфігурації. Одним із найкорисніших інструментів для полегшення цього процесу є Spring Boot Properties Migrator — компонент, який автоматично допомагає виявляти застарілі налаштування та пропонує їх сучасні альтернативи. Що таке Spring Boot Properties Migrator? Spring Boot Properties Migrator — це спеціальна бібліотека, яка…

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Цікаве

Dependency Injection у Spring: пояснюємо на пальцях

  • Автор javaadmin
  • 31 Липня, 2026
  • 15 views
Dependency Injection у Spring: пояснюємо на пальцях

Java Collections Framework: List, Set чи Map?

  • Автор javaadmin
  • 31 Липня, 2026
  • 29 views
Java Collections Framework: List, Set чи Map?

Оптимізуємо це: GraalVM Native Image, Leyden та CRaC

  • Автор javaadmin
  • 17 Липня, 2026
  • 57 views
Оптимізуємо це: GraalVM Native Image, Leyden та CRaC

JBang: Як вивчати Java легко

  • Автор javaadmin
  • 12 Травня, 2026
  • 131 views
JBang: Як вивчати Java легко

Agent Skills: Що це таке і чому про це варто знати

  • Автор javaadmin
  • 13 Квітня, 2026
  • 235 views
Agent Skills: Що це таке і чому про це варто знати

Dependency Injection в Java

  • Автор javaadmin
  • 9 Грудня, 2025
  • 408 views
Dependency Injection в Java