Inversion of Control (IoC) and Dependency Injection (DI)
Inversion of Control (IoC)
Inversion of Control (IoC) is a principle in software design where the control of object creation and management is transferred from the object itself to an external entity, often called a container or framework. This approach allows for loose coupling between components in a system.
IoC is commonly implemented through Dependency Injection (DI), where objects declare their dependencies, and the IoC container provides those dependencies at runtime.
Dependency Injection (DI)
Dependency Injection is a design pattern that provides a way to supply an object with its required dependencies. The main ways to perform dependency injection are:
- Constructor Injection: Dependencies are provided through the constructor of the class.
- Setter Injection: Dependencies are provided through setter methods.
- Interface Injection: Dependencies are provided through an interface implemented by the dependent class.
IoC and DI in Action
Let’s consider a scenario where we have an OrderService that relies on a PaymentProcessor for processing payments. Without
IoC, the OrderService would directly instantiate PaymentProcessor, leading to tight coupling.
Without IoC (Tightly Coupled Code)
class PaymentProcessor { void processPayment(double amount) { System.out.println("Processing payment of: " + amount); }}
class OrderService { private PaymentProcessor paymentProcessor;
public OrderService() { this.paymentProcessor = new PaymentProcessor(); // Direct instantiation }
void placeOrder(double amount) { paymentProcessor.processPayment(amount); System.out.println("Order placed successfully."); }}
public class Main { public static void main(String[] args) { OrderService orderService = new OrderService(); orderService.placeOrder(100.0); }}In this example:
OrderServicedirectly depends on thePaymentProcessorimplementation.- This tight coupling makes it difficult to switch to a different
PaymentProcessorimplementation or testOrderServiceindependently.
With IoC and Dependency Injection (Loosely Coupled Code)
Using an IoC container, we can make OrderService and PaymentProcessor loosely coupled:
Step 1: Define Interfaces and Implementations
interface PaymentProcessor { void processPayment(double amount);}
class CreditCardProcessor implements PaymentProcessor { @Override public void processPayment(double amount) { System.out.println("Processing credit card payment of: " + amount); }}
class PayPalProcessor implements PaymentProcessor { @Override public void processPayment(double amount) { System.out.println("Processing PayPal payment of: " + amount); }}Step 2: Refactor OrderService
class OrderService { private PaymentProcessor paymentProcessor;
// Constructor Injection public OrderService(PaymentProcessor paymentProcessor) { this.paymentProcessor = paymentProcessor; }
void placeOrder(double amount) { paymentProcessor.processPayment(amount); System.out.println("Order placed successfully."); }}Step 3: Configure IoC Container
In a Spring configuration file or using annotations:
@Configurationclass AppConfig { @Bean public PaymentProcessor paymentProcessor() { return new CreditCardProcessor(); }
@Bean public OrderService orderService(PaymentProcessor paymentProcessor) { return new OrderService(paymentProcessor); }}Step 4: Run the Application
public class Main { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); OrderService orderService = context.getBean(OrderService.class); orderService.placeOrder(100.0); }}Key Advantages of IoC and DI
- Loose Coupling: Objects depend on abstractions (interfaces) rather than concrete implementations.
- Improved Testability: Dependencies can be easily mocked or stubbed in unit tests.
- Flexibility: Switching between different implementations of a dependency is straightforward.
- Enhanced Maintainability: Centralized configuration makes it easier to manage changes.