So Explain To Me What Is Dependency Injection

Apple fruit with plastic syringes
Photo by Sara Bakhshi on Unsplash

So what is dependency injection? What does it do for the design of your application? What are the benefits (and drawbacks) of dependency injection? In this post I’ll explain what it is, and provide some Java code examples to show you how to use it.

Dependency Injection is one of the five principles of object oriented design. These principles help you design and develop cleaner code that is easier to read, understand, maintain, and is more robust and more maintainable.

Dependency injection also goes by the name Inversion of Control. It simply means there is no new keyword in Java.

To illustrate this, I’ll start with a code sample. Let’s suppose you have a service class that uses a DAO (Data Access Object) class to talk to a database. That service class might look like this:

package ca.airspeed.injection.service;

import ca.airspeed.injection.dao.AircraftDao;
import ca.airspeed.injection.dao.JdbcAircraftDao;
import ca.airspeed.injection.domain.Aircraft;

public class AircraftService {
    private JdbcAircraftDao aircraftDao;

    public AircraftService() {
        this.aircraftDao = new JdbcAircraftDao();  // Notice this!
    }

    public Aircraft findByRegistration(String registration) {
        return aircraftDao.findByRegistration(registration);
    }
}

Notice the constructor instantiates a DAO class. When you use dependency injection, that service class now looks like this:

package ca.airspeed.injection.service;

import ca.airspeed.injection.dao.AircraftDao;
import ca.airspeed.injection.domain.Aircraft;
import org.springframework.stereotype.Service;

@Service
public class AircraftService {
    private AircraftDao aircraftDao;

    public AircraftService(AircraftDao aircraftDao) {
        this.aircraftDao = aircraftDao;  // The DAO class is injected here.
    }

    public Aircraft findByRegistration(String registration) {
        return aircraftDao.findByRegistration(registration);
    }

    AircraftDao getAircraftDao() {
        return this.aircraftDao;
    }
}

Now, instead of our service class instantiating the collaborating DAO class, it relies on something else to provide it with one. That “something else” is the dependency injection framework. Spring Framework, Google Guice, and Micronaut are the more popular ones, with Spring Framework arguably being the market leader.

What I’ve shown here is an example of constructor injection. The other types of injection are setter injection, and field injection. Each uses the annotation @Inject (or @Autowired in Spring Framework). The former annotates a setter method, and the latter annotates a private field.

Did you see the @Service annotation on the AircraftService class? That tells Spring Framework to instantiate a singleton bean of this class. Spring will see the one-argument constructor, and go looking for an AircraftDao implementation that has a similar annotation; typically that annotation is @Repository for a persistence layer class:

package ca.airspeed.injection.dao;

import ca.airspeed.injection.domain.Aircraft;
import com.mysql.cj.jdbc.MysqlDataSource;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;

@Repository
public class JdbcAircraftDao implements AircraftDao{

    private DataSource dataSource;

    public JdbcAircraftDao() {
        this.dataSource = new MysqlDataSource();
        // Set the URL, user, password, etc.
    }

    @Override
    public Aircraft findByRegistration(String registration) {
        // A bunch of JDBC statements go here.
        return Aircraft.builder().build();
    }
}

With dependency injection, you usually specify the collaborating class as an interface, and provide a concrete implementation of that interface that you annotated appropriately. So in our case, we changed the AircraftService class so its collaborator is the interface AircraftDao. Then we provided the implementation JdbcAircraftDao, an annotated it with @Repository.

The nice part about this is it provides loose-coupling. The AircraftService class knows it will get some implementation of an AircraftDao. What that specific implementation is, the service class doesn’t know, nor does it really care. It can concentrate on business logic, and leave the persistence details up to some other class.

Show me a Conceptual View

Moving up to a 10,000-foot view, here is what dependency injection looks like:

We have our AircraftService that uses some implementation of an AircraftDao. The dependency injection framework will find a bean that implements the AircraftDao interface, and inject that bad boy into AircraftService.

What Problem does Dependency Injection Solve?

Scaleability

Using the new keyword when only a handful of people or fewer are using your application is not a big deal. But ratchet that up to hundreds, thousands, tens of thousands or more, then it becomes a problem. This is because it’s instantiating a new object every time a method on the collaborating class is invoked. That takes up memory. Sooner or later you’ll run into heap space issues. Users will suffer periodic lags in response times because of garbage collection.

Sure, you could use a factory class that creates and returns a singleton every time you use the collaborating class, but why clutter your code with such boilerplate logic? Let the dependency injection framework manage these singletons. These are known as beans in Spring Framework.

Testability

Suppose you go to write a unit test for AircraftService, and let’s say it instantiates its own JdbcAircraftDao as in the first code example above. All you want to do is assert the correct functionality of AircraftService. But now you have a JdbcAircraftDao to deal with in your test case, and it uses a database. So you need to figure out how to connect to the database, and seed it with data. Can you see how much of a pain this is going to be? Talk about a rabbit hole!

With dependency injection, you can mock out the AircraftDao by providing a test double, or you can use a mocking framework such as Mockito. Now you can focus on just the AircraftService, writing assertions just for that class.

Dependency injection nudges you toward a clean design, where each class does one thing, and does it well. This is the Single Responsibility Principle of object-oriented design. You can write a unit test for the one class under test, and just mock its collaborators.

Maintainability

Since dependency injection encourages a clean design, you end up with a lower risk of change. The changes you make to the collaborating class are isolated to that class, provided it keeps the promises it made to the primary class. A comprehensive suite of unit tests goes a long way to ensuring it does so.

Wrapping Up

Dependency injection pushes you toward clean code that is scaleable, testable, and maintainable. It helps reduce the risk of change since it nudges you toward adopting the Single Responsibility Principle that isolates changes only to the class(es) that need to change.

Share this: