The WHY Series: Why should you use dependency injection?

If we want to be better devs, we oughta know why we use a piece of code, a design pattern, or an architectural decision, rather than just because an elder dev told us to.

We’ve looked at why our controllers should be thin (looked at that twice, actually), and why we should use the repository pattern.

Let’s hit another buzzword, why don’t we?

Why should you use dependency injection?

In software design, there are a myriad of strategies spawned for the sole purpose of easing the friction.

Friction is any kind of mucking about a developer has to do to a pre-existing component. To put it simply, it’s changing code. Areas of code that are constantly changing are high friction points where bugs form, so anything that can be done to support code stability is usually favorable.

Look at the friction caused by hardcoding a string.

Hardcoding is called hardcoding because there is no way to change it without a code change.

Since code changes are expensive and risky, we solve the hardcoding problem with configuration files and table-stored application settings so that things we have found to change a lot like user options, database connection strings, and environment variables can be changed without changing the code that needs them.

Ease the friction just like a brake pad

How ‘bout a little explain it like I’m five analogy action?

Easing the friction is also something car manufacturers do with high friction parts like brake pads, and air filters.

Your car slows down by extreme hydraulic and electric force clamping onto the wheels. Because so much heat builds up in that action, car manufacturers used a material that won’t catch fire easily, but that also wears out faster than other parts and needs to be replaced frequently.

Because they need to change often, car designers have made it quite easy to replace them.

Remove the wheel and loosen a couple bolts and the old pads will slide right out!

This is precisely the ingenuity at play when we develop strategies like configuration files, and, at last, dependency injection.

What is dependency injection?

Dependency injection is simply the act of passing a class everything it needs, rather than having the class create what it needs for itself.

For example, let’s look at a theoretical API endpoint that lets users make payments.

Perhaps for the sake of time, you built the endpoint to only support Google Pay for payments, so you simply created an instance of the ‘GooglePay’ service class right inside the controller like so:

1
2
3
4
5
6
7
8
public class PaymentsController : Controller
{
    private readonly GooglePay _googlePay;

    public PaymentsController()
    {
        _googlePay = new GooglePay();
    }

This is fine if you only intend to support GooglePay, but your users now want to pay with Apple Pay, Stripe, PayPal, and bitcoin, and your boss wants heavy unit testing around this area.

Because of this, we now see that making the controller instantiate a GooglePay instance is exactly like hardcoding a string.

Imagine if the GooglePay() constructor had multiple complex parameters (that also had parameters). Your controller would have to know how to create all these dependencies and hardcode them in.

So much friction for the poor guy.

To ease this friction, we can do two things. First, keeping GooglePay() behind an abstract base class or an interface will allow us to abstract away the details of the service from the controller. That way it can also work with other services like Apple Pay without changing the method calls.

1
2
3
4
5
6
7
8
9
public class PaymentsController : Controller
{
    private readonly PaymentService _paymentService;

    public PaymentsController()
    {
        _paymentService = new GooglePay();
    }
    

Secondly, the dependency injection pattern can be used to pass a previously created PaymentService instance to the controller (in this case by way of the constructor), thus injecting it in like a syringe from the outside in.

Our controller has no idea whether it’s using Google Pay, Apply Pay, Stripe, or any other service, it just has to follow the rules of the PaymentService interface and move on with life.

1
2
3
4
5
6
7
8
9
public class PaymentsController : Controller
{
    private readonly PaymentService _paymentService;

    public PaymentsController(PaymentService paymentService)
    {
        _paymentService = paymentService;
    }
    

In the same way we replaced a hardcoded string with a value retrieved from a configuration file, we are using dependency injection to replace a hardcoded dependency to one created elsewhere.

Now, we of course still need to implement the damn bitcoin and Apple Pay services, or the mock payment service for unit testing, but now we can do so without changing the controller.

We’ve made the controller code stable.

Less reasons to change it. Less chances for bugs.

Easing the friction.

Now, the magic behind when (or by what object) the service is passed to the controller is beyond the scope of this article, but I’ve spent some time digging through the asp.net core code and reading probably 10 to 15 articles in an effort to get a good understanding of it.

Instead of making you wait until I synthesize it into a new article, I’ve put together an ASP.NET Core Dependency Injection Roundup document that compiles a list of high quality articles with my thoughts on each of them, as well as some early edition article thoughts and ideas. You’ll also get the finished article when it’s ready.

Get the ASP.NET Core Dependency Injection Roundup!