Skip to Content
Course content

111: Dependency Injection as a Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're building a high-end coffee machine. If you weld the water tank directly into the chassis, the machine works great—until the tank leaks or you decide you want a bigger one. Now, to change the tank, you have to take a blowtorch to the entire machine. That's a nightmare.

Instead, you design the machine with a standard hose connection on the back. The machine doesn't care if the water comes from a plastic jug, a fancy filtration system, or directly from the city plumbing. As long as the source fits that hose and provides water, the machine does its job. You've decoupled the "making coffee" logic from the "getting water" logic.

In C#, Dependency Injection (DI) is that hose connection. Here is how that maps to your code:

  • The Coffee Machine is your high-level class (the one that does the main work).
  • The Water Source is the dependency (a service, a database, or an API client).
  • The Hose Connection is the Interface. It defines the contract of what the dependency must do, without caring how it does it.
  • The Act of Plugging it in is the "Injection."

The Problem with Hard-Coding Your Dependencies

Most developers start by doing things the "welded" way. Let's say you're building a system to send order confirmations. You might write something like this:

public class OrderProcessor
{
    private readonly EmailSender _emailSender = new EmailSender();

    public void ProcessOrder(Order order)
    {
        // Process the order...
        _emailSender.Send("Order confirmed!");
    }
}

At first glance, this is fine. But look closely: OrderProcessor is now married to EmailSender. If your boss tells you tomorrow that confirmations should go via SMS, or if you want to write a unit test without actually sending 500 real emails to your customers, you're stuck. You have to change the code inside OrderProcessor, which risks breaking your order logic just to change a notification method. I've spent way too many hours fixing bugs introduced by "simple" changes like this.

Letting the Outside World Decide

To fix this, we stop using the new keyword inside our classes. Instead, we define an interface and ask for it in the constructor. This is the heart of the DI pattern.

public interface IMessageService
{
    void Send(string message);
}

public class OrderProcessor
{
    private readonly IMessageService _messageService;

    // We don't create the service; we just demand that someone gives us one.
    public OrderProcessor(IMessageService messageService)
    {
        _messageService = messageService;
    }

    public void ProcessOrder(Order order)
    {
        // Process the order...
        _messageService.Send("Order confirmed!");
    }
}

Now, OrderProcessor is blissfully ignorant of how the message is sent. It just knows that whatever object it receives can call Send(). You've effectively moved the decision-making process "up the chain" to the part of your app that starts everything up.

Why We Actually Do This (Beyond Just Being Fancy)

You might be thinking, "This feels like a lot of extra typing just to avoid a new keyword." Trust me, it's worth it for two main reasons.

First: Testability. In a unit test, you don't want to hit a real database or a real email API. With DI, you can inject a "Mock" or "Fake" version of the service that just records that the method was called, making your tests lightning fast and reliable.

Second: Parallel Development. I can write the OrderProcessor logic while a teammate writes the SmsService. As long as we both agree on the IMessageService interface, our code will slot together perfectly without us needing to wait for each other to finish.




📋 Practical Task

Refactor the PaymentGateway for Multiple Providers

You have been handed a PaymentProcessor class that is currently "welded" to a specific PayPal implementation. Your goal is to refactor this using the Dependency Injection pattern so it can support any payment provider (like Stripe or Square) without changing the processor's internal code.

Requirements:

  1. Create an interface named IPaymentProvider with a method bool ProcessPayment(decimal amount).
  2. Create a class PayPalProvider that implements this interface (simply return true and print "Processing via PayPal").
  3. Modify the PaymentProcessor class to remove the hard-coded new PayPalProvider() call.
  4. Implement Constructor Injection in PaymentProcessor to receive an IPaymentProvider.
  5. In your Main method, instantiate a PayPalProvider and inject it into the PaymentProcessor.
// Start with this coupled code:
public class PayPalProvider 
{
    public bool ProcessPayment(decimal amount) => true;
}

public class PaymentProcessor
{
    private readonly PayPalProvider _provider = new PayPalProvider();
    
    public void Checkout(decimal total)
    {
        _provider.ProcessPayment(total);
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.