Dependency injection is a means, not an end
When I discovered dependency injection, or DI, about six years ago as a way to remove tight coupling between objects, it felt like the Holy Grail of programming. The widespread adoption of the Spring Framework and the inclusion of DI in the frameworks and standards that followed made it a normal part of programming today. Unfortunately, we use it without thinking, even where we don't need it.
In the previous paragraph I called DI a way to do something. Yes, DI is a way of implementing what's known as inversion of control. Service Locator is another. The point is to spare our objects from having to know more than necessary about other objects, because that creates tight coupling. Loose coupling between objects makes them easier to test and brings other advantages.
Buoyed by this mantra, we started creating complex graphs in XML to describe how our objects relate. For every hundred lines of code we gained twenty lines of XML descriptors. And what did we get for it? A headache when refactoring class and package names, and the joy of tracking down why something didn't work, only to discover that some clever person had renamed a bean.
The obvious question is when to use DI and when to keep tight coupling between objects. I have two rules. Rule one: If it's infrastructure (messaging, transactions, blah blah blah) or something to do with deployment, always use DI. Nothing's worse than muddying the business logic with infrastructure code. Rule two: Make DI possible while letting the object work with a tightly coupled dependency.
public class SweetService {
private final LittleDao littleDao;
public SweetService() {
this(new LittleDao());
}
public SweetService(LittleDao littleDao) {
this.littleDao = littleDao;
}
}The code above illustrates exactly that approach. I've deliberately used the overworked terms Service and DAO. I assume the reader knows what they mean, so I don't have to explain the code further. The SweetService class can create a LittleDao instance on its own and can also accept one as a constructor argument. If I ever need a specialized LittleDao, I can still use the constructor that takes it as an argument. I do this whenever I test the class with Mockito and pass in mocks so I can verify how it behaves.
In 99% of cases your ordinary objects will get by with a default constructor that contains tightly coupled dependencies. If construction starts getting more complicated, ask whether you're creating a class that wants to be the alpha male. That's a good time to step back from the code and try to split it into smaller objects with clear responsibilities.
So what do we get out of it? Code that's independent of its surroundings. An object can work without being registered somewhere. Fewer integration tests to cover external configuration, which affects how fast the tests run and how quickly you find out something broke. Faster application startup, depending on how much configuration the IoC container has to process. An application and codebase that are easier to understand, since anything outside the code is hard to find and maintain.
Speaking of configuration, annotations are an option and remove some disadvantages of external configuration and XML. Annotations are a good choice, but they have drawbacks: tight coupling to a concrete type, the inability to specify a default implementation (in the Spring Framework), and the eventual need to mix them with XML because some things can't be expressed with annotations.
If DI is a means rather than an end, then good object-oriented design should be the end. DI can help a great deal, as long as you think about each use in its broader context.
Related articles
Related CZ Podcast episodes
- #261 · Leadership
On leading people and teams. · Czech audio
- #3 · Java 6 SE, Vývoj webových aplikací
Dagi and Filemon discuss Java 6 and web application frameworks. · Czech audio
- #1 · Vývojová prostředí v Javě
The first CZ Podcast episode looks at Java development environments. · Czech audio