Dependency injection: Difference between revisions
→Basics: improved for clarity |
Modified code example; the factory should not be instanced. |
||
Line 120: | Line 120: | ||
public static void main(String[] args) { |
public static void main(String[] args) { |
||
Car car = |
Car car = CarFactory.buildCar(); |
||
int speed = car.onAcceleratorPedalStep(5); |
int speed = car.onAcceleratorPedalStep(5); |
||
logWithFormat("The car's speed is %s.", speed); |
logWithFormat("The car's speed is %s.", speed); |
Revision as of 17:51, 20 November 2009
Dependency injection (DI) in computer programming refers to the process of supplying an external dependency to a software component. It is a specific form of inversion of control where the concern being inverted is the process of obtaining the needed dependency. The term was first coined by Martin Fowler to describe the mechanism more clearly.[1]
Basics
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.
Such a pattern involves at least three elements: a dependent, its dependencies and an injector (sometimes referred to as a provider or container). The dependent is a consumer that needs to accomplish a task in a computer program. In order to do so, it needs the help of various services (the dependencies) that execute certain sub-tasks. The provider is the component that is able to compose the dependent and its dependencies so that they are ready to be used, while also managing these objects' life-cycles. This provider may be implemented, for example, as a service locator, an abstract factory, a factory method or a more complex abstraction such as a framework.
The following is an example. A car (the consumer) depends upon an engine (the dependency) in order to move. The car's engine is made by an automaker (the dependency provider). The car does not know how to install an engine into itself, but it needs an engine in order to move. The automaker installs an engine into the car and the car utilizes the engine to move.
When the concept of dependency injection is used, it decouples high-level modules from low-level services. The result is called the dependency inversion principle.
Code illustration using Java
Using the car/engine example above mentioned, the following Java examples show how coupled dependencies, manually injected dependencies, and framework-injected dependencies are typically staged.
For these examples, assume the Engine
interface and an implied SimpleEngine
default implementation.
public interface Engine {
public void setFuelValveIntake(int gasPedalPressure);
public int getTorque();
}
public interface Car {
public int onAcceleratorPedalStep(int gasPedalPressure);
}
Highly coupled dependency
The following shows a common arrangement with no dependency injection applied:
public class SimpleEngine implements Engine{
int gasPedalPressure=0;
int torque=0;
/** @set Fuel capacity*/
public void setFuelValveIntake(int gasPedalPressure){
this.gasPedalPressure=gasPedalPressure;
}
/** @return Torque*/
public int getTorque(){
return torque;
}
}
public class SimpleCar implements Car {
private Engine engine = new SimpleEngine();
/** @returns the car speed */
public int onAcceleratorPedalStep(int gasPedalPressure) {
engine.setFuelValveIntake(gasPedalPressure);
int torque = engine.getTorque();
int speed = ... //math to get the car speed from the engine torque
return speed;
}
}
public class MyApplication {
public static void main(String[] args) {
Car car = new SimpleCar();
int speed = car.onAcceleratorPedalStep(5);
logWithFormat("The car's speed is %s.", speed);
}
}
As shown, the Car
class needs to create an instance of an Engine
to calculate its speed based on how much pressure is made on the accelerator pedal. A similar implementation would instantiate the Engine
inside the onAcceleratorPedalStep
method, but such an approach still forces the Car
class to know how to instantiate its own Engine
.
Note that because each Car
has its own engine, it is possible to have Car
create its own. In this artificial example, Car
owns the Engine
, so this is relatively simple. In other examples where you may have a shared dependency, such as a database access subsystem, no dependent owns the dependency, rather the dependency is loaned. In such a case the dependency would outlive the dependent, or be shared by a large number of dependents, so each dependent could not simply create its own version without resorting to workarounds in the language, such as static methods that bypass the object's encapsulation boundary.
Manually injected dependency
Now, should this example use dependency injection, a possible implementation might be:
public class Car {
private final Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
/** @returns the car speed */
public int onAcceleratorPedalStep(int gasPedalPressure) {
engine.setFuelValveIntake(gasPedalPressure);
int torque = engine.getTorque();
int speed = ... //math to get the car speed from the engine torque
return speed;
}
}
public class CarFactory {
public static Car buildCar() {
return new Car(new SimpleEngine());
}
}
public class MyApplication {
public static void main(String[] args) {
Car car = CarFactory.buildCar();
int speed = car.onAcceleratorPedalStep(5);
logWithFormat("The car's speed is %s.", speed);
}
}
In the code above, the factory takes responsibility for assembling the car, and injects the engine into the car. This frees the car from knowing about how to create an engine, though now the CarFactory
has to know about the engine's construction. One could create an EngineFactory
, but that merely creates dependencies among factories. So the problem remains, but has at least been shifted into factories, or builder objects.
Framework-managed dependency injection
There are several frameworks available to further automate this process, allowing a complete delegation of instantiation and marshalling of such dependencies to a "meta-factory", often called a Container or Injector. An example using such a framework could look like this:
// Note the same Car implementation, but there's a Car interface declared elsewhere
public class CarImpl extends Car {
private final Engine engine;
public CarImpl(Engine engine) {
this.engine = engine;
}
/** @returns the car speed */
public int onAcceleratorPedalStep(int gasPedalPressure) {
engine.setFuelValveIntake(gasPedalPressure);
int torque = engine.getTorque();
int speed = ... //math to get the car speed from the engine torque
return speed;
}
}
// Note no factory - just the main application
public class MyApplication {
public static void main(String[] args) {
Container c = new DependencyInjectorContainer();
c.bind(Car.class,CarImpl.class);
c.bind(Engine.class, SimpleEngine.class);
c.init();
Car car = c.getObject(Car.class); // asks injector/container for a car.
int speed = car.onAcceleratorPedalStep(5);
logWithFormat("The car's speed is %s.", speed);
}
}
In this example, a Dependency Injection Container is used, and bindings are declared between CarImpl
and Car
, then between SimpleEngine
and Engine
. After initialization, the container, when asked for a car, will note that the type of Car
registered is a CarImpl
. It then observes that CarImpl
has a constructor with one argument - an Engine
, implying that this CarImpl
must have an Engine
to be a valid Car
. It therefore attempts to create an Engine
, noting that the type of Engine
it has registered is a SimpleEngine
. It creates this (since SimpleEngine
in this example has no further dependencies), and passes that Engine
to the CarImpl
, which it then returns.
This pattern can be followed with a small or large number of declared objects and their dependencies. This basic pattern, with enhancements based on different scopes and lifecycles, is typical of most Dependency Injection frameworks.
This example does not show the full potential of dependency injection since the CarFactory
is very simple. In a simple case such as the above, managing the dependencies requires slightly more code than the naive no-injection approach. However, as components get more and more complex, or if shared instances of dependencies are required, preventing owned dependencies from being created by their dependents, then such a framework can provide minimal code to manage a large number of objects and their creation and dependency fulfillment.
Benefits and issues
One important benefit of using dependency injection approach is the reduction of boilerplate code in the application objects since all work to initialize or setup dependencies will be made in the provider component[2].
Also, it offers more flexibility because it becomes easier to create alternative implementations of a given service type, and then to specify which implementation is to be used via a configuration file, without any change to the objects that use the service. This is especially useful in unit testing, because it is easy to inject a fake implementation of a service into the object being tested.
On the other hand, excessive or inappropriate use of dependency injection can make applications more complex and harder to maintain. Code that uses dependency injection can seem magical to some developers, since instantiation and initialization of code is handled completely separately from that code's normal operation. This separation, while useful for maintenance, can also result in subtleties that can be hard to diagnose. Additionally, some dependency-injection frameworks maintain verbose configuration files, requiring that a developer understand the configuration as well as the code, where such configuration is "invisible" to IDE-supported reference analysis and refactoring. Some IDEs mitigate against this by providing explicit support for such frameworks. Other such frameworks provide configuration using the programming language itself, allowing such refactoring support to apply directly. Other frameworks such as the Grok web framework introspect the code and use convention over configuration as an alternative form of deducing configuration information. For example, if a Model and View class were in the same module, then an instance of the View will be created with the appropriate Model instance passed into the constructor.
Types
Fowler identifies three ways in which an object can get a reference to an external module, according to the pattern used to provide the dependency:[3]
- Type 1 or interface injection, in which the exported module provides an interface that its users must implement in order to get the dependencies at runtime.
- Type 2 or setter injection, in which the dependent module exposes a setter method that the framework uses to inject the dependency.
- Type 3 or constructor injection, in which the dependencies are provided through the class constructor.
It is possible for other frameworks to have other types of injection, beyond those presented above.[4]
Existing frameworks
Dependency injection frameworks exist for a number of platforms and languages, as can be seen in the following table:
See also
Further reading
- A beginners guide to Dependency Injection
- What is Dependency Injection? - An alternative explanation - Jakob Jenkov
- Dependency Injection & Testable Objects: Designing loosely coupled and testable objects - Jeremy Weiskotten; Dr. Dobb's Journal, May 2006.
- Design Patterns: Dependency Injection -- MSDN Magazine, September 2005
- Writing More Testable Code with Dependency Injection -- Developer.com, October 2006
- Domain Specific Modeling (DSM) in IOC frameworks
- The Rich Engineering Heritage Behind Dependency Injection - Andrew McVeigh - A detailed history of dependency injection.
- P of EAA: Plugin
References
This article includes a list of references, related reading, or external links, but its sources remain unclear because it lacks inline citations. (October 2007) |