Dlaczego Dependency Injection zmieni Twój sposób programowania
Dependency Injection to **fundamentalna koncepcja Spring Framework**, która rozwiązuje jeden z największych problemów obiektowego programowania – zarządzanie zależnościami między klasami. **Bez DI Twój kod staje się sztywny, trudny do testowania i mocno powiązany**. Z DIzyskujesz elastyczność, testowalność i kod, który można łatwo rozwijać.
Co się nauczysz:
- Czym jest Dependency Injection i Inversion of Control
- Jak skonfigurować Spring Container z XML i adnotacjami
- Różnice między Constructor, Setter i Field Injection
- Praktyczne przykłady DI w aplikacjach biznesowych
- Najlepsze praktyki i częste błędy w DI
- Jak testować kod wykorzystujący Spring DI
Problem – mocno powiązany kod bez DI
Zacznijmy od przykładu **ZŁEGO** kodu, który nie używa Dependency Injection:
// PRZYKŁAD ZŁEGO KODU - bez Dependency Injection public class OrderService { private EmailService emailService; private DatabaseService databaseService; public OrderService() { // Sztywne tworzenie zależności! this.emailService = new EmailService(); this.databaseService = new MySQLDatabaseService(); } public void processOrder(Order order) { // Zapisz zamówienie databaseService.save(order); // Wyślij email potwierdzający emailService.sendConfirmation(order.getCustomerEmail(), order); } }
**Problemy tego podejścia:**
1. **Mocne powiązanie** – OrderService jest ściśle związany z konkretnymi implementacjami
2. **Brak elastyczności** – nie można łatwo zmienić implementacji EmailService
3. **Trudne testowanie** – jak przetestować OrderService bez wysyłania prawdziwych emaili?
4. **Naruszenie Single Responsibility** – klasa zarządza swoimi zależnościami
Rozwiązanie – Dependency Injection w akcji
Oto ten sam kod przepisany z użyciem Spring DI:
// PRZYKŁAD DOBREGO KODU - z Dependency Injection @Service public class OrderService { private final EmailService emailService; private final DatabaseService databaseService; // Constructor Injection - najlepsza praktyka @Autowired public OrderService(EmailService emailService, DatabaseService databaseService) { this.emailService = emailService; this.databaseService = databaseService; } public void processOrder(Order order) { // Logika biznesowa bez martwienia się o zależności databaseService.save(order); emailService.sendConfirmation(order.getCustomerEmail(), order); } } @Service public class EmailService { public void sendConfirmation(String email, Order order) { // Logika wysyłania emaila System.out.println("Wysyłam email do: " + email); } } @Repository public class DatabaseService { public void save(Order order) { // Logika zapisywania do bazy System.out.println("Zapisuję zamówienie: " + order.getId()); } }
Konfiguracja Spring Container – XML vs Adnotacje
W Spring 4 mamy dwie główne metody konfiguracji:
Konfiguracja XML (tradycyjna)
Konfiguracja adnotacjami (nowoczesna – Spring 4)
@Configuration @ComponentScan(basePackages = "com.example") public class AppConfig { // Spring automatycznie znajdzie klasy oznaczone @Service, @Repository, @Component } // Główna klasa aplikacji public class Application { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); OrderService orderService = context.getBean(OrderService.class); Order order = new Order("12345", "jan@example.com"); orderService.processOrder(order); } }
Typy Dependency Injection w Spring
1. Constructor Injection (zalecane)
@Service public class OrderService { private final EmailService emailService; private final DatabaseService databaseService; @Autowired public OrderService(EmailService emailService, DatabaseService databaseService) { this.emailService = emailService; this.databaseService = databaseService; } }
**Zalety Constructor Injection:**
– Gwarantuje że wszystkie zależności są dostarczzone przy tworzeniu obiektu
– Umożliwia tworzenie immutable obiektów (final fields)
– Łatwiejsze testowanie – jasno widać co jest potrzebne
2. Setter Injection
@Service public class OrderService { private EmailService emailService; private DatabaseService databaseService; @Autowired public void setEmailService(EmailService emailService) { this.emailService = emailService; } @Autowired public void setDatabaseService(DatabaseService databaseService) { this.databaseService = databaseService; } }
**Kiedy używać Setter Injection:**
– Opcjonalne zależności
– Circular dependencies (choć to anti-pattern)
– Legacy kod
3. Field Injection
@Service public class OrderService { @Autowired private EmailService emailService; @Autowired private DatabaseService databaseService; }
Praktyczny przykład – System sklepu internetowego
// Interfejsy - luźne powiązanie public interface PaymentProcessor { boolean processPayment(BigDecimal amount, String cardNumber); } public interface InventoryService { boolean reserveProduct(String productId, int quantity); void releaseReservation(String productId, int quantity); } // Implementacje @Service public class CreditCardProcessor implements PaymentProcessor { @Override public boolean processPayment(BigDecimal amount, String cardNumber) { // Logika płatności kartą System.out.println("Przetwarzam płatność: " + amount + " PLN"); return true; } } @Service public class DatabaseInventoryService implements InventoryService { @Override public boolean reserveProduct(String productId, int quantity) { // Sprawdź dostępność w bazie System.out.println("Rezerwuję " + quantity + " szt. produktu " + productId); return true; } @Override public void releaseReservation(String productId, int quantity) { System.out.println("Zwalniam rezerwację " + productId); } } // Główny serwis biznesowy @Service public class OrderProcessingService { private final PaymentProcessor paymentProcessor; private final InventoryService inventoryService; private final EmailService emailService; @Autowired public OrderProcessingService(PaymentProcessor paymentProcessor, InventoryService inventoryService, EmailService emailService) { this.paymentProcessor = paymentProcessor; this.inventoryService = inventoryService; this.emailService = emailService; } @Transactional public void processOrder(Order order) { try { // 1. Sprawdź dostępność produktów for (OrderItem item : order.getItems()) { if (!inventoryService.reserveProduct(item.getProductId(), item.getQuantity())) { throw new InsufficientStockException("Brak towaru: " + item.getProductId()); } } // 2. Przetwórz płatność boolean paymentSuccess = paymentProcessor.processPayment( order.getTotalAmount(), order.getPaymentDetails().getCardNumber() ); if (!paymentSuccess) { // Zwolnij rezerwacje releaseAllReservations(order); throw new PaymentException("Błąd płatności"); } // 3. Wyślij potwierdzenie emailService.sendOrderConfirmation(order); } catch (Exception e) { releaseAllReservations(order); throw e; } } private void releaseAllReservations(Order order) { for (OrderItem item : order.getItems()) { inventoryService.releaseReservation(item.getProductId(), item.getQuantity()); } } }
Testowanie kodu z Dependency Injection
Oto jak łatwo testować kod używający DI:
import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class OrderProcessingServiceTest { @Mock private PaymentProcessor paymentProcessor; @Mock private InventoryService inventoryService; @Mock private EmailService emailService; @InjectMocks private OrderProcessingService orderService; @Test public void shouldProcessOrderSuccessfully() { // Given Order order = createTestOrder(); when(inventoryService.reserveProduct(any(), anyInt())).thenReturn(true); when(paymentProcessor.processPayment(any(), any())).thenReturn(true); // When orderService.processOrder(order); // Then verify(paymentProcessor).processPayment(order.getTotalAmount(), order.getPaymentDetails().getCardNumber()); verify(emailService).sendOrderConfirmation(order); } @Test(expected = PaymentException.class) public void shouldReleaseReservationsWhenPaymentFails() { // Given Order order = createTestOrder(); when(inventoryService.reserveProduct(any(), anyInt())).thenReturn(true); when(paymentProcessor.processPayment(any(), any())).thenReturn(false); // When orderService.processOrder(order); // Then - exception thrown, reservations released verify(inventoryService).releaseReservation(any(), anyInt()); } private Order createTestOrder() { // Helper method to create test data return new Order("123", "customer@example.com", new BigDecimal("99.99")); } }
Bean Scopes – zarządzanie cyklem życia obiektów
Scope | Opis | Kiedy używać |
---|---|---|
singleton | Jeden obiekt na cały kontener (domyślne) | Bezstanowe serwisy, DAO |
prototype | Nowy obiekt przy każdym wywołaniu | Obiekty ze stanem, krótko żyjące |
request | Jeden obiekt na HTTP request | Web aplikacje, request-specific data |
session | Jeden obiekt na HTTP session | User-specific data, koszyk |
@Service @Scope("singleton") // Domyślne - można pominąć public class EmailService { // Jeden obiekt dla całej aplikacji } @Component @Scope("prototype") public class ShoppingCart { // Nowy obiekt przy każdym getBean() private Listitems = new ArrayList<>(); }
Częste błędy i najlepsze praktyki
// BŁĄD - circular dependency @Service public class UserService { @Autowired private OrderService orderService; // OrderService też używa UserService! } // ROZWIĄZANIE - wprowadź interfejs lub event @Service public class UserService { @Autowired private ApplicationEventPublisher eventPublisher; public void createUser(User user) { // ... logika tworzenia eventPublisher.publishEvent(new UserCreatedEvent(user)); } }
**Najlepsze praktyki DI:**
1. **Używaj Constructor Injection** jako domyślnej opcji
2. **Preferuj interfejsy** zamiast konkretnych klas
3. **Unikaj Field Injection** w kodzie produkcyjnym
4. **Jednoznaczne nazwy beanów** gdy masz wiele implementacji
5. **@Qualifier dla rozróżnienia** implementacji tego samego interfejsu
// Wiele implementacji tego samego interfejsu @Service @Qualifier("creditCard") public class CreditCardProcessor implements PaymentProcessor { } @Service @Qualifier("paypal") public class PayPalProcessor implements PaymentProcessor { } // Użycie konkretnej implementacji @Service public class OrderService { private final PaymentProcessor paymentProcessor; public OrderService(@Qualifier("creditCard") PaymentProcessor paymentProcessor) { this.paymentProcessor = paymentProcessor; } }
Domyślnie tak – wszystkie singleton beany są tworzone eager (przy starcie). Możesz to zmienić adnotacją @Lazy dla lazy initialization.
Aplikacja nie wystartuje i rzuci NoSuchBeanDefinitionException. Możesz użyć @Autowired(required = false) dla opcjonalnych zależności.
Tak! Spring automatycznie wstrzyknie List lub Map wszystkich beanów danego typu: @Autowired List<PaymentProcessor> processors
Włącz logi Spring na poziomie DEBUG, użyj @PostConstruct do sprawdzenia czy beany są prawidłowo utworzone, lub skorzystaj z Spring Boot Actuator.
Narzut jest minimalny – Spring używa reflection do wstrzykiwania, ale to dzieje się tylko przy starcie aplikacji. W runtime nie ma praktycznie żadnego narzutu.
Tak! DI to wzorzec projektowy. Możesz go implementować ręcznie lub użyć innych kontenerów IoC jak Google Guice czy CDI.
To centralny interfejs Spring IoC Container. Zarządza beanami, ich konfiguracją i cyklem życia. To „serce” aplikacji Spring.
Przydatne zasoby:
- Spring Reference – IoC Container
- Spring Reference – Annotation-based Configuration
- Martin Fowler – Inversion of Control Containers
- Spring 4 JavaDoc
🚀 Zadanie dla Ciebie
Stwórz prostą aplikację z trzema serwisami: UserService (zarządza użytkownikami), NotificationService (wysyła powiadomienia) i AuditService (loguje operacje). Użyj Constructor Injection i napisz testy jednostkowe z mockami. Bonus: dodaj dwie implementacje NotificationService (Email i SMS) i użyj @Qualifier.
Jak zamierzasz wykorzystać Dependency Injection w swoich projektach? Podziel się swoimi doświadczeniami z testowaniem kodu używającego Spring DI!