Wednesday, April 21, 2010

Dependency Injection

http://en.wikipedia.org/wiki/Dependency_injection

Without the concept of dependency injection, a consumer who needs a particular service in order to accomplish a certain task would be responsible for handling the life-cycle (instantiating, opening and closing streams, disposing, etc.) of that service. Using the concept of dependency injection, however, the life-cycle of a service is handled by a dependency provider (typically a container) rather than the consumer. The consumer would thus only need a reference to an implementation of the service that it needed in order to accomplish the necessary task.

However, there should exist a definite reason for moving a dependency away from the object that needs it because doing so can complicate the code hierarchy to such an extent that its usage appears to be "magical".

Benefits:
  • One benefit of using the dependency injection approach is the reduction of boilerplate code in the application objects since all work to initialize or setup dependencies is handled by a provider component
  • Another benefit is that it offers configuration flexibility because alternative implementations of a given service can be used without recompiling code. This is useful in unit testing because it is easy to inject a fake implementation of a service into the object being tested by changing the configuration file.
Drawbacks:
  • One drawback is that excessive or inappropriate use of dependency injection can make applications more complicated, harder to understand and more difficult to modify. Code that uses dependency injection can seem magical to some developers, since instantiation and initialization of objects is handled completely separately from the code that uses it. This separation can also result in problems that are hard to diagnose. Additionally, some dependency injection frameworks maintain verbose configuration files, requiring that a developer understand the configuration as well as the code in order to change it.
  • Another drawback is that some IDEs might not be able to accurately analyze or refactor code when configuration is "invisible" to it. Some IDEs mitigate this problem by providing explicit support for various frameworks.

Manually-injected dependency

Refactoring the above example to use manual injection:
public class DefaultCarImpl implements Car {
private Engine engine;

public DefaultCarImpl(Engine engineImpl) {
engine = engineImpl;
}

public float getSpeedInMPH() {
return engine.getEngineRPM() * ...;
}

public void setPedalPressure(float pedalPressureInPounds) {
engine.setFuelConsumptionRate(...);
}
}

public class CarFactory {
public static Car buildCar() {
return new DefaultCarImpl(new DefaultEngineImpl());
}
}

public class MyApplication {
public static void main(String[] args) {
Car car = CarFactory.buildCar();
car.setPedalPressure(5);
float speed = car.getSpeedInMPH();
}
}
In the example above, the CarFactory class assembles a car and an engine together by injecting a particular engine implementation into a car. This moves the dependency management from the Car class into the CarFactory class. However, this still may not be enough abstraction for some applications.