Dependency Injection — What’s the point?

Christopher Tanghare
Christopher Tanghare
Contents

Several years ago, as a naive junior developer who had worked primarily in C++, I was asked to start on a new project using C#. Naturally, I did what any foolish and inexperienced developer would do, I and started writing the code the same way that one would write it in C++. After feeling like I did a great job and requesting a code review, I was prepared to receive a pat on the back for a job well done (and maybe even a promotion!) Much to my surprise, I received a comment similar to this one: “You should really consider using Dependency Injection in this project. You do know what Dependency Injection is, right?” Not wanting to look stupid, I replied:

“Me? Ha! Of course I do! Dependency Injection is practically my middle name! This was just a prototype. Wait till you see the real thing.”

Shortly afterward, I took the proverbial developer walk of shame as I went directly to Google with questions like…

What is dependency injection?

Why should I use it?

How does it make my program better?

This article will attempt to answer those questions and provide a real business use case that I wrote use using .NET Core, Autofac, and Mock unit testing.

What is Dependency Injection?

Dependency Injection (DI) is a design pattern that helps you write code that is loosely coupled and achieves the important principle of Inversion of Control (IoC). We’ll see an example shortly that should clarify what that means and how it works.

Why should you use Dependency Injection?

There are many benefits of using Dependency Injection and other similar principles. Dependency Injection offers many benefits, including (but not limited to):

  1. Making it easier for your program to follow the single responsibility principle, which is the “S” in the commonly used SOLID acronym.
  2. Assisting in creating modular, reusable code.
  3. Working to separate creation of an object from its usage.
  4. Naturally pushing you toward loosely coupled code.
  5. Enabling you to replace a dependency without needing to modify the calling module’s code.
  6. Allowing you to take advantage of Mock objects to truly unit test your work and your work alone.

Are you sold yet? Let’s look at a real life example to see how we can improve a program using this principle.

Business Use Case

Recently, I was asked to write a program that receives trucker’s real-time GPS positions from RabbitMQ and creates notifications related to the user’s trip. For example, we want to detect if the the driver is approaching or departing a stop. Shameless plug, but if solving problems like this sound interesting to you, we’d love for you to apply to our Trimble Maps team here!

Getting back to our example, if our program finds the position is within a certain threshold distance of a stop, it will write some relevant info to a database. Simple enough, right? In order to do this, we have a few obvious dependencies:

  1. Reading messages from RabbitMQ.
  2. Reading the “next stop” from a database.
  3. Writing the updated information to a database.

For our first attempt, let’s create a program with a traditional code structure, like I did as a junior developer.

Code Version 1.0

What are the problems?

  1. Our class is doing way more than one thing — this violates the Single Responsibility Principle.
  2. The code is not modular; no one can ever reuse it.
  3. It’s tightly coupled and difficult to change independent pieces.
  4. It is completely impossible to unit test. Our code relies on connections to RabbitMQ and database(s) in order to function, so we can really only test the entire thing all at once using an integration test.
  5. If even one component is broken, all of our integration tests will fail. Problems like this are painful to debug, speaking from experience!

Now we’re going to improve the code using Dependency Injection by creating Interfaces which will be passed in or “injected” into our class through the constructor. This helps us to achieve the “D” in the SOLID acronym — the Dependency Inversion Principle — which states that higher level modules should not depend on lower level modules, and that modules should depend on abstractions rather than on concrete details. Our code will now depend on the abstraction of an interface rather than relying on the concretion of an instance of a class.

Code Version 2.0

We clearly improved our code by using DI, but can we do better? A painful point that you may encounter when using DI is that passing in all these components and setting up each dependency (and all of the dependency’s dependencies) can become cumbersome and annoying to do each time we want to create an instance of our class. Also consider that I am making this problem much more simple than it really is for the purpose of this article. In reality, you can easily have 20+ dependencies in your dependency chain in code that has a high degree of modularity.

Solving this difficult, tangled web of dependency management is where the concept of an Inversion of Control (IoC) Container comes in. An IoC container helps us alleviate this problem by managing our dependencies for us, along with providing several other handy features to make our jobs easier. For our purpose, we’ll use Autofac, but there are several other good options available as well.

Code Version 3.0

Let’s improve the code again to move some of the logic out of GPSProcessor and take advantage of the IoC Container. According to the Single Responsibility Principle, the GPSProcessor class should be processing GPS Positions, not calculating distance. A good idea would be to move the distance calculation code to a new class and create an interface with one public function. You might be thinking, “One public function? That’s crazy!” No, it’s actually a positive “code smell”, because it indicates that each class has a single responsibility. Don’t be afraid of adding a bunch of new, smaller files to your solution; new files are cheap, but bad code is expensive.

Autofac IoC Container

This code will handle dependency setup and management by creating single instances of our classes that we can now inject easily. In a more complicated program, this can be incredibly useful as we are able to reuse components multiple times to be injected in different modules.

How have we made our program better? Of course, the benefits that we discussed previously apply, but I want to point out two of them which I think are real game changers:

If we wanted to change the class that implements one of these interfaces later on, we can do that seamlessly. Say we have the StopStatusRepository class which implements the IStopStatusWriter interface. A few weeks later, we get a new request to improve our application’s performance, so we decide to write to a memory cache rather than a database. All we would need to do is create a StopStatusCache class that implements IStopStatusWriter, and then we don’t even need to change any code in our GPSProcessor class! We’ll just change the object that we’re injecting, and then we’re good to go. That’s the beauty of Dependency Injection in action!

Now, we can unit test our code using Mock testing. We don’t actually need a RabbitMQ or database connection for our unit test to work. In fact, we shouldn’t have them at all for a unit test. We just want to test the logic of this specific component — the GPSProcessor. We can leave the RabbitMQ and database connections to be tested where they belong, in integration testing. Our unit tests will prove that our component does exactly what it’s supposed to do and nothing further than that.

Unit Testing

Here is a small sample of how we can unit test our code. This is a contrived example of course; in reality, you’ll want your unit testing to be much more thorough and detailed, and I’d probably break these classes down even further. However, I hope this example will give you a good starting point into understanding how you can reap the benefits of the powerful tool that Dependency Injection is.

Share this article:

You May Also Like