DEV Community

Cover image for A straightforward introduction to Dependency Injection
Jorge Cifuentes
Jorge Cifuentes

Posted on • Updated on • Originally published at jorgecf.github.io

A straightforward introduction to Dependency Injection

This content was published here before.

This post presents a concise and short introduction to the technique called Dependency Injection (DI).

The task

Let's say you have a database with user-related data and that you want to query some of this data to process it. You might start with a class looking like this, using a SqlReader class:

public class Foo {

    SqlReader dataReader;

    constructor() {
        this.dataReader = new SqlReader();
    }

    public double Process(int userId) {
        var data = this.dataReader.QueryById(userId);
        // process the data
        // ...
    }        
}
Enter fullscreen mode Exit fullscreen mode

The problem

Imagine that your software expands and, when running in a new mode, it should not get the data from a database but from some CSV files.

The main obstacle here is that our Foo class is coupled with the SqlReader: you can see it in the fact that a new instance is created in the constructor. In short words, it depends on an implementation: SqlReader is a dependency of Foo.

One could add this CSV functionality to the SqlReader class (ugly) or make CsvReader expand SqlReader (would get messy when adding even new more readers). This is a perfect use case for inversion of control.

The inversion of control

Theory

Here is where the concept Dependency Inversion gets to shine. Simply speaking, it means that the control of any class dependencies should be inverted: the Foo class shouldn't be the one instantiating a SqlReader.

This can be accomplished with the technique called Dependency Injection: it applies the principle that ensures classes are never responsible of supplying their own dependencies (they get injected).

And why is it important? It helps you decouple your application: you have the implemented code in one side, and the code that uses it in the other, both depending on a common interface. You can change them separately as long as it fits the interface, thanks to the concerns being separated.

Alt Text

The code

Usually, you would start by creating an interface IReader:

public class IReader {
    public List<Data> QueryById(int id);
}
Enter fullscreen mode Exit fullscreen mode

and implement it for every reader you wanted:

public class SqlReader : IReader {
    public List<Data> QueryById(int id) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode
public class CsvReader : IReader {
    public List<Data> QueryById(int id) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Interfaces? Nothing new

You probably already knew about interfaces and, at the end of the day, an object based on one has to be instantiated with a concrete implementation. So how is it better if you're going to end up doing this?:

public class Foo {

    IReader dataReader;

    constructor() {
        this.dataReader = new SqlReader(); // <--- coupling
    }

    public double Process(int userId) {
        var data = this.dataReader.QueryById(userId);
        // process the data
        // ...
    }       
}
Enter fullscreen mode Exit fullscreen mode



This is where the dependency inversion container plays its part: somewhere in your code, you still need to instantiate the implementation of the interface. The container is the one actually instantiating your objects and supplying them, so your code will look like this:

public class Foo {

    IReader dataReader;

    constructor(IReader r) { // <--- this is called by the container
        this.dataReader = r; // <--- injection
    }

    public double Process(int userId) {
        var data = this.dataReader.QueryById(userId);
        // process the data
        // ...
    }       
}
Enter fullscreen mode Exit fullscreen mode

The container is a fairly complicated software that tracks and manages how interfaces are implemented. You could simplify them in your mind as a dictionary of interfaces mapped to implementing classes. Some examples of these containers are Autofac or Ninject.

With Autofac you can do something in the lines of this to have multiple implementations depending on some runtime variable:

if (someCondition)
    builder.RegisterType<SqlReader>().As<IReader>();
else
    builder.RegisterType<CsvReader >().AsIReader>();
Enter fullscreen mode Exit fullscreen mode

The magic thing about containers, as opposed to wiring the dependencies yourself, is that you don't have to worry about calling the constructor with new SqlReader(). This is very useful with a lot of interfaces where the dependency chain is deeply nested. You just tell the container what implementation you want to use and let it inject it using the constructor in whatever number of classes you might have.

After these changes, total decoupling is achieved. Foo doesn't have to know any details of IReader or even how its instantiated: it just gets injected.

Note that containers usually can inject dependencies via the constructor, or a property or field.

So what is it useful for?

Extensibility and reusability

The first thing to note is that given your code is based on abstracts, extending it's as easy as implementing new classes and telling the container under which circumstances they should be used. The same code using IReader can work with a SqlReader, CsvReader, JsonReader or any other implementation. You can change and switch these classes without having to change the code that uses them.

Unit testing

If you wanted to test the Foo.Process method, hitting your database when running tests would be, most of the time, an anti-pattern and slow.

But, since Foo depends on the interface IReader, you can resolve this interface to SqlReader in normal execution and FakeDataReader while running tests. The FakeDataReader would just fake the database using in-memory variables. That would mean faster tests and in a more controlled and reproducible environment.

Safer parallel programming

Two developers can work with classes that use each other only based on the interface, without having to modify the other developer files. Less git conflicts ๐Ÿ˜ƒ.

The cons

Obviously, the main adverse effect is the added layer of indirection. This makes the software a bit harder to understand - your implemented classes won't be directly referenced where they will be used. You won't be able to find references or easily trace them, and you won't know which implementation is injected until you debug it.

Wrap-up

To summarise, dependency injection is a technique used to satisfy the dependency inversion principle, introducing interfaces between a high-level class and its dependencies and making their relation loosely coupled assuring that the only central point where an interface is related to its actual implementation is in the container code.

  1. Get your high-level class dependencies injected via the constructor (dependency injection).
  2. Make this dependencies implement a common interface (decoupling).
  3. Leave the hassle of calling all the injecting constructors to a dependency inversion container.

As a sidenote, using interfaces isn't inherent to dependency injection: you can have dependencies marked as a normal class and have them injected, but DI works better with interfaces.

At the end of the day, using this technique is a development decision up the creator. Any questions?

Discussion (1)

Some comments have been hidden by the post's author - find out more