Tuesday, June 26, 2012

Dependency injection for beginners


Dependency injection for beginners

Dependency injection, also known as Inversion of control is an often used term in the Java community. It may sound sophisticated, even scary, to beginners. This article aims to explain what dependency injection is in the simplest way possible. The goal is for every Java developer to understand what this term is all about.

Dependency injection is a way of developing software that makes it _testable_, i.e. by leveraging dependency injection, you allow your classes to be tested. We achieve that by passing (injecting) dependencies. That's really the main idea behind dependency injection.

The simplest example illustrating the concept:

1. Code without dependency injection
public void processPerson(Integer personId) {
  DatabaseConnection con = ConnectionManager.getConnection("localhost:4342");
  ResultSet rs = con.execute("select * from Person where personId=" + personId);
  Person person = new Person();
  person.setName(rs.get(0));
  person.setSurname(rs.get(1));
  if (person.getName()==null) {
    person.setName("unknown");
  }
}

Is this code testable? Sure it is. But in order to test it, you need to have a SQL database running on your machine with exact data that fits your test (i.e. a person with specific data and personId). This has a number of issues:
- your tests are many lines long and hard to write
- your tests are slow because a the test methods need to read from the database
- your tests become flaky because they rely on data stored in the database. If that data changes (or the database login changes for example), your tests will behave incorrectly
The last point is the most serious one because everything might be working nicely for a while and your tests can fail for no apparent reason after a while.

The code above has a dependency on the database connection which is created within the method. This makes the method very hard to test as we can't change the behavior of the database connection, i.e. it will always connect to an database server.

1. Code with dependency injection
public void processPerson(Integer personId, IConnection connection) {
  ResultSet rs = connection.execute("select * from Person where personId=" + personId);
  Person person = new Person();
  person.setName(rs.get(0));
  person.setSurname(rs.get(1));
  if (person.getName()==null) {
    person.setName("unknown");
  }
}

This code requires the connection (the dependency for the method to work properly) to be injected. From a testing perspective this is great, because we can create a test implementation of the IConnection interface, like so:

class TestConnection implements IConnection {
  public ResultSet execute(String sql) {
    String name = null;
    String surname = "TestSurname";
    ResultSet rs = new ResultSet(name, surname);
    return rs;
  }
}

...and it would hence simulate a Person with null set as name and "TestSurname" set as surname.

Advantages of dependency injection:
- external dependencies become transparent and they are no longer hidden in the business logic
- makes code testable by enabling to change the behavior of external dependencies
- tests are super fast as you create small objects isolated from the external environment
- tests are deterministic (i.e. no longer flaky) because no matter what happens in the environment, you'll get the same results from your tests

P.S.: I realize that senior software developers wouldn't write such code as I used in my example. It breaks the separation of concerns principle because the processPerson method deals with both retrieving the person and executing some business logic on the person. This should not happen in a clean OO design. I deliberately crafted the above code, as experience tells me many many developers would write such code and hence it's a good way to demonstrate dependency injection on it.


No comments:

Post a Comment