Mastering Inversion of Control (IoC) and Dependency Injection (DI) with Real-World Examples .Net Core
Introduction
In software development, managing dependencies between classes is crucial for building flexible, maintainable systems. Inversion of Control (IoC) and Dependency Injection (DI) are two key principles that help achieve this, allowing your code to stay organized and adaptable. In this blog, I'll demonstrate these concepts using a real-world example of a food delivery payment structure, showing how they can simplify and enhance your application's architecture.
What is Inversion of Control (IoC) and Dependency Injection (DI)?
Inversion of Control (IoC)
Inversion of Control is a principle used to decouple components in a system. It essentially means that the control of creating and managing object instances is inverted from the traditional approach. Instead of a class creating its dependencies, an external entity handles this responsibility.
In simple terms, IoC means "Don't Call Me, I'll Call You." Instead of having classes create their dependencies, they rely on an external mechanism to provide those dependencies.
Dependency Injection (DI)
Dependency Injection is one of the ways to achieve IoC. It involves providing an object with its dependencies from the outside rather than having the object create them itself. This can be done in various ways:
Constructor Injection: Passing dependencies through a class constructor.
Property Injection: Setting dependencies through properties.
Method Injection: Passing dependencies as method parameters.
Service Locator: Using a service locator to obtain dependencies at runtime.
In essence, DI is how one object gets its dependencies from another object.
Real-World Scenario: Food Delivery App
Imagine you are building a food delivery app. This app needs to process payments, and you might use different payment gateways like PayPal or Razorpay. Here’s how IoC and DI can be applied to manage these dependencies effectively.
Problem: Fixed Payment Gateway
Without IoC and DI, you might directly instantiate the payment gateway service within the OrderProcessor
class. This approach tightly couples OrderProcessor
with a specific payment gateway, making the system rigid and hard to maintain.
public class OrderProcessor
{
private PayPalPaymentService _paymentService;
public OrderProcessor()
{
_paymentService = new PayPalPaymentService(); // Tightly coupled
}
public void ProcessOrder()
{
_paymentService.ProcessPayment();
}
}
public class PayPalPaymentService
{
public void ProcessPayment()
{
Console.WriteLine("Processing payment via PayPal.");
}
}
Disadvantages
Tight Coupling:
OrderProcessor
is tightly coupled withPayPalPaymentService
. Any change toPayPalPaymentService
requires modifyingOrderProcessor
.Limited Extensibility: Adding support for other payment gateways requires changes to
OrderProcessor
.Difficult Testing: Testing
OrderProcessor
becomes cumbersome as it directly depends on a specific implementation.
Introducing IoC and DI
With IoC and DI, we can refactor the OrderProcessor
to depend on abstractions rather than concrete implementations.
public interface IPaymentService
{
void ProcessPayment();
}
public class PayPalPaymentService : IPaymentService
{
public void ProcessPayment()
{
Console.WriteLine("Processing payment via PayPal.");
}
}
public class RazorpayPaymentService : IPaymentService
{
public void ProcessPayment()
{
Console.WriteLine("Processing payment via Razorpay.");
}
}
public class OrderProcessor
{
private readonly IPaymentService _paymentService;
public OrderProcessor(IPaymentService paymentService)
{
_paymentService = paymentService;
}
public void ProcessOrder()
{
_paymentService.ProcessPayment();
}
}
Dependency Injection Setup
Here’s how you might set up DI in a .NET Core application:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IPaymentService, PayPalPaymentService>(); // For example, PayPal
services.AddTransient<OrderProcessor>();
}
Advantages of IoC and DI
Loose Coupling: The
OrderProcessor
class is no longer dependent on a specific payment service implementation. It only depends on theIPaymentService
interface.Flexibility: Easily switch between different implementations of
IPaymentService
without modifying theOrderProcessor
class.Testability: Simplifies unit testing by allowing mock implementations of
IPaymentService
to be injected.
Introducing Dynamic Selection with the Factory Pattern
While IoC and DI enhance flexibility, sometimes dynamic selection of implementations based on user input or configuration is required. The Factory Pattern can address this need by providing a way to create objects dynamically.
Example: Selecting Payment Gateway Dynamically
In a real-world scenario, a user might select a payment gateway such as PayPal or Razorpay. Using the Factory Pattern, we can create and configure the appropriate IPaymentService
implementation based on user selection.
Factory Pattern Implementation
Basic understanding: Imagine you're at a toy store. Instead of picking out toys yourself, you tell the store clerk what type of toy you want (like a car, a doll, or a robot). The clerk then goes to the back room and brings out the specific toy you asked for rather than you going to pick the toys yourself.
In this analogy:
The toy store is the factory.
The clerk is the factory method.
The different toys (car, doll, robot) are the products.
The Factory Pattern works the same way in programming. Instead of creating objects directly in your code, you ask a "factory" to create them for you based on what you need. This keeps your code cleaner and more flexible.
When to Use It?
Dynamic Object Creation: When the exact type of object to create isn't known until runtime.
Complex Object Construction: When creating objects involves complex logic or multiple steps.
Code Decoupling: When you want to decouple the creation of objects from their usage.
Let's break down the code and explain how everything works together, especially in the context of an API where the user selects a payment gateway (e.g., PayPal or Razorpay), and the system processes the payment accordingly.
Define Payment Gateway Interfaces
public interface IPaymentService
{
void ProcessPayment();
}
public class PayPalPaymentService : IPaymentService
{
public void ProcessPayment()
{
Console.WriteLine("Processing payment via PayPal.");
}
}
public class RazorpayPaymentService : IPaymentService
{
public void ProcessPayment()
{
Console.WriteLine("Processing payment via Razorpay.");
}
}
Payment Service Interfaces and Factories:
public interface IPaymentServiceFactory
{
IPaymentService CreatePaymentService();
}
public class PayPalPaymentFactory : IPaymentServiceFactory
{
public IPaymentService CreatePaymentService()
{
return new PayPalPaymentService();
}
}
public class RazorpayPaymentFactory : IPaymentServiceFactory
{
public IPaymentService CreatePaymentService()
{
return new RazorpayPaymentService();
}
}
- Factory Provider:
public interface IPaymentServiceFactoryProvider
{
IPaymentServiceFactory GetPaymentServiceFactory(string paymentGateway);
}
public class PaymentServiceFactoryProvider : IPaymentServiceFactoryProvider
{
public IPaymentServiceFactory GetPaymentServiceFactory(string paymentGateway)
{
return paymentGateway switch
{
"PayPal" => new PayPalPaymentFactory(),
"Razorpay" => new RazorpayPaymentFactory(),
_ => throw new NotSupportedException($"Payment gateway '{paymentGateway}' is not supported.")
};
}
}
- OrderProcessor class
public class OrderProcessor
{
private readonly IPaymentService _paymentService;
public OrderProcessor(IPaymentService paymentService)
{
_paymentService = paymentService;
}
public void ProcessOrder()
{
_paymentService.ProcessPayment();
}
}
- Controller Action
public class PaymentController : ControllerBase
{
private readonly IPaymentServiceFactoryProvider _factoryProvider;
public PaymentController(IPaymentServiceFactoryProvider factoryProvider)
{
_factoryProvider = factoryProvider;
}
[HttpPost("process-payment")]
public IActionResult ProcessPayment(string paymentGateway)
{
var factory = _factoryProvider.GetPaymentServiceFactory(paymentGateway);
var paymentService = factory.CreatePaymentService();
var orderProcessor = new OrderProcessor(paymentService);
orderProcessor.ProcessOrder();
return Ok("Payment processed successfully.");
}
}
- Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IPaymentServiceFactoryProvider, PaymentServiceFactoryProvider>();
// Add controllers
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
Flowchart Representation
Here’s a simple flowchart to visualize the process:
User Selects Payment Gateway
↓Controller Receives Selection
↓Factory Provider Determines Which Factory to Use
↓Factory Creates the Appropriate Payment Service
↓Order Processor Uses the Payment Service to Process Payment
↓Payment is Processed
Conclusion
Inversion of Control (IoC) and Dependency Injection (DI) are powerful principles for managing dependencies and creating flexible, maintainable code. While IoC and DI provide significant benefits, the Factory Pattern adds further flexibility by allowing dynamic creation of objects based on configuration or user input. By combining these techniques, we can build scalable and adaptable applications that meet various user needs and preferences.
Feel free to share your thoughts and experiences with IoC, DI, and the Factory Pattern in the comments below. Happy coding!
Subscribe to my newsletter
Read articles from Ujjwal Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Ujjwal Singh
Ujjwal Singh
👋 Hi, I'm Ujjwal Singh! I'm a software engineer and team lead with 10 years of expertise in .NET technologies. Over the years, I've built a solid foundation in crafting robust solutions and leading teams. While my core strength lies in .NET, I'm also deeply interested in DevOps and eager to explore how it can enhance software delivery. I’m passionate about continuous learning, sharing knowledge, and connecting with others who love technology. Let’s build and innovate together!