Dependency injection
In software engineering, dependency injection is a software design pattern that implements inversion of control for software libraries. Caller delegates to an external framework the control flow of discovering and importing a service or software module specified or "injected" by the caller. Dependency injection allows a program design to follow the dependency inversion principle where modules are loosely coupled. With dependency injection, the client which uses a module or service doesn't need to know all its details, and typically the module can be replaced by another one of similar characteristics without altering the client.
An injection is the passing of a dependency (a service) to a dependent object (a client). The service is made part of the client's state. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.
There are three common forms of dependency injection: setter-, interface- and constructor-based injection, where the responsibility of injecting the dependency lies upon the client, the service or the constructor method respectively.
Overview
Dependency injection is a software design pattern in which one or more dependencies (or services) are injected, or passed by reference, into a dependent object (or client) and are made part of the client's state. The pattern separates the creation of a client's dependencies from its own behavior, which allows program designs to be loosely coupled and to follow the dependency inversion and single responsibility principles.[1][2] It directly contrasts with the service locator pattern, which allows clients to know about the system they use to find dependencies.
Dependency injection involves four elements: the implementation of a service object; the client object depending on the service; the interface the client uses to communicate with the service; and the injector object, which is responsible for injecting the service into the client. The injector object may also be referred to as an assembler, provider, container, factory, or spring.
Implementation of dependency injection is often identical to that of the strategy pattern, but while the strategy pattern is intended for dependencies to be interchangeable throughout an object's lifetime, in dependency injection only a single instance of a dependency is used.
Application frameworks such as Spring, Guice, Glassfish HK2, and Microsoft Managed Extensibility Framework (MEF) support dependency injection.
Advantages
- Because dependency injection doesn't require any change in code behavior it can be applied to legacy code as a refactoring. The result is clients that are more independent and that are easier to unit test in isolation using stubs or mock objects that simulate other objects not under test. This ease of testing is often the first benefit noticed when using dependency injection.
- Dependency injection allows a client to remove all knowledge of a concrete implementation that it needs to use. This helps isolate the client from the impact of design changes and defects. It promotes reusability, testability and maintainability.[3]
- Dependency injection can be used to externalize a system's configuration details into configuration files allowing the system to be reconfigured without recompilation. Separate configurations can be written for different situations that require different implementations of components. This includes, but is not limited to, testing.
- Reduction of boilerplate code in the application objects since all work to initialize or set up dependencies is handled by a provider component.[3]
- Dependency injection allows concurrent or independent development. Two developers can independently develop classes that use each other, while only needing to know the interface the classes will communicate through. Plugins are often developed by third party shops that never even talk to the developers who created the product that uses the plugins.
- Dependency Injection decreases coupling between a class and its dependency. [4]
Disadvantages
- Dependency injection can make code difficult to trace (read) because it separates behavior from construction. This means developers must refer to more files to follow how a system performs.
- Dependency injection typically requires more lines of code to accomplish the same behavior legacy-code would.
- Dependency injection increases coupling by requiring the user of a subsystem to provide for the needs of that subsystem.[5]
- Dependency injection diminishes encapsulation by requiring users of a system to know how it works internally and not merely what task of job it performs.[6]
Examples
Without dependency injection
In the following Java example, the Client
class contains a Service
member variable that is initialized by the Client
constructor. The client controls which implementation of service is used and controls its construction. In this situation, the client is said to have a hard-coded dependency on ServiceExample()
.
// An example without dependency injection public class Client { // Internal reference to the service used by this client private Service service; // Constructor Client() { // Specify a specific implementation in the constructor instead of using dependency injection this.service = new ServiceExample(); } // Method within this client that uses the services public String greet() { return "Hello " + service.getName(); } }
Dependency injection is an alternative technique to initialize the member variable than explicitly creating a service object as shown above.
Three types of dependency injection
There are at least three ways an object can receive a reference to an external module:[7]
- constructor injection: the dependencies are provided through a class constructor.
- setter injection: the client exposes a setter method that the injector uses to inject the dependency.
- interface injection: the dependency provides an injector method that will inject the dependency into any client passed to it. Clients must implement an interface that exposes a setter method that accepts the dependency.
Other types
It is possible for frameworks to have other types of injection beyond those presented above.[8]
Some attempts at inversion of control do not provide full removal of dependency but instead simply substitute one form of dependency for another. As a rule of thumb, if a programmer can look at nothing but the client code and tell what framework is being used, then the client has a hard-coded dependency on the framework.
Constructor injection
This method requires the client to provide a parameter in a constructor for the dependency.
// Constructor Client(Service service) { // Save the reference to the passed-in service inside this client this.service = service; }
Setter injection
This method requires the client to provide a setter method for each dependency.
// Setter method public void setService(Service service) { // Save the reference to the passed-in service inside this client this.service = service; }
Interface injection
This is simply the client publishing a role interface to the setter methods of the client's dependencies. It can be used to establish how the injector should talk to the client when injecting dependencies.
// Service setter interface. public interface ServiceSetter { public void setService(Service service); } // Client class public class Client implements ServiceSetter { // Internal reference to the service used by this client. private Service service; // Set the service that this client is to use. @Override public void setService(Service service) { this.service = service; } }
Constructor injection comparison
Preferred when all dependencies can be constructed first because it can be used to ensure the client object is always in a valid state, as opposed to having some of its dependency references be null (not be set). However, on its own, it lacks the flexibility to have its dependencies changed later.
// Constructor Client(Service service, Service otherService) { if (service == null) { throw new InvalidParameterException("service must not be null"); } if (otherService == null) { throw new InvalidParameterException("otherService must not be null"); } // Save the service references inside this client this.service = service; this.otherService = otherService; }
Setter injection comparison
Requires the client to provide a setter method for each dependency. This gives the freedom to manipulate the state of the dependency references at any time. This offers flexibility, but if there is more than one dependency to be injected, it is difficult for the client to ensure that all dependencies are injected before the client could be provided for use.
// Set the service to be used by this client public void setService(Service service) { if (service == null) { throw new InvalidParameterException("service must not be null"); } this.service = service; } // Set the other service to be used by this client public void setOtherService(Service otherService) { if (otherService == null) { throw new InvalidParameterException("otherService must not be null"); } this.otherService = otherService; }
Since these injections happen independently there is no way to tell when the injector is finished wiring the client. A dependency can be left null simply by the injector failing to call its setter. This forces the check that injection was completed from when the client is assembled to whenever it is used.
// Set the service to be used by this client public void setService(Service service) { this.service = service; } // Set the other service to be used by this client public void setOtherService(Service otherService) { this.otherService = otherService; } // Check the service references of this client private void validateState() { if (service == null) { throw new IllegalStateException("service must not be null"); } if (otherService == null) { throw new IllegalStateException("otherService must not be null"); } } // Method that uses the service references public void doSomething() { validateState(); service.doYourThing(); otherService.doYourThing(); }
Interface injection comparison
The advantage of interface injection is that dependencies can be completely ignorant of their clients yet can still receive a reference to a new client and, using it, send a reference-to-self back to the client. In this way, the dependencies become injectors. The key is that the injecting method (which could just be a classic setter method) is provided through an interface.
An assembler is still needed to introduce the client and its dependencies. The assembler would take a reference to the client, cast it to the setter interface that sets that dependency, and pass it to that dependency object which would turn around and pass a reference-to-self back to the client.
For interface injection to have value, the dependency must do something in addition to simply passing back a reference to itself. This could be acting as a factory or sub-assembler to resolve other dependencies, thus abstracting some details from the main assembler. It could be reference-counting so that the dependency knows how many clients are using it. If the dependency maintains a collection of clients, it could later inject them all with a different instance of itself.
Assembling examples
Assembling in main by hand is only one way of implementing dependency injection.
public class Injector { public static void main(String[] args) { // Build the dependencies first Service service = new ServiceExample(); // Inject the service, constructor style Client client = new Client(service); // Use the objects System.out.println(client.greet()); } }
Frameworks like Spring allow assembly details to be externalized in configuration classes and annotations.
import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Injector { public static void main(String[] args) { // Assemble the objects BeanFactory beanfactory = new AnnotationConfigApplicationContext(MyConfiguration.class); Client client = beanfactory.getBean(Client.class); // Use the objects System.out.println(client.greet()); } }
Spring will construct these objects and wire them together before returning a reference to the client.
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @ComponentScan static class MyConfiguration { @Bean public Client client(ServiceExample service) { return new Client(service); } } @Component public class ServiceExample { //... }
Assembly comparison
The different injector implementations (factories, service locators, and dependency injection containers) are not that different as far as dependency injection is concerned. What makes all the difference is where they are allowed to be used. Move calls to a factory or a service locator out of the client and into main and it suddenly makes a fairly good dependency injection container.
By moving all knowledge of the injector out, a clean client, free of knowledge of the outside world, is left behind. However, any object that uses other objects can be considered a client. The object that contains main is no exception. This main object is not using dependency injection. It's actually using the service locator pattern. This can't be avoided because the choice of service implementations must be made somewhere.
Externalizing the dependencies into configuration files doesn't change this fact. What makes this reality part of a good design is that the service locator is not spread throughout the code base. It's confined to one place per application. This leaves the rest of the code base free to use dependency injection to make clean clients.
AngularJS example
In the AngularJS framework, there are only three ways a component (object or function) can directly access its dependencies:
- The component can create the dependency, typically using the new operator.
- The component can look up the dependency, by referring to a global variable.
- The component can have the dependency passed to it where it is needed.
The first two options of creating or looking up dependencies are not optimal because they hard code the dependency to the component. This makes it difficult, if not impossible, to modify the dependencies. This is especially problematic in tests, where it is often desirable to provide mock dependencies for test isolation.
The third option is the most viable, since it removes the responsibility of locating the dependency from the component. The dependency is simply handed to the component.
function SomeClass(greeter) { this.greeter = greeter; } SomeClass.prototype.doSomething = function(name) { this.greeter.greet(name); }
In the above example SomeClass
is not concerned with creating or locating the greeter dependency, it is simply handed the greeter when it is instantiated.
This is desirable, but it puts the responsibility of getting hold of the dependency on the code that constructs SomeClass
.
To manage the responsibility of dependency creation, each AngularJS application has an injector. The injector is a service locator that is responsible for construction and look-up of dependencies.
Here is an example of using the injector service:
// Provide the wiring information in a module var myModule = angular.module('myModule', []); // Teach the injector how to build a greeter service. // Notice that greeter is dependent on the $window service. // The greeter service is an object that // contains a greet method. myModule.factory('greeter', function($window) { return { greet: function(text) { $window.alert(text); } }; });
Create a new injector that can provide components defined in the myModule
module and request our greeter service from the injector. (This is usually done automatically by the AngularJS bootstrap).
var injector = angular.injector(['myModule', 'ng']); var greeter = injector.get('greeter');
Asking for dependencies solves the issue of hard coding, but it also means that the injector needs to be passed throughout the application. Passing the injector breaks the Law of Demeter. To remedy this, we use a declarative notation in our HTML templates, to hand the responsibility of creating components over to the injector, as in this example:
<div ng-controller="MyController"> <button ng-click="sayHello()">Hello</button> </div> function MyController($scope, greeter) { $scope.sayHello = function() { greeter.greet('Hello World'); }; }
When AngularJS compiles the HTML, it processes the ng-controller
directive, which in turn asks the injector to create an instance of the controller and its dependencies.
injector.instantiate(MyController);
This is all done behind the scenes. Notice that by having the ng-controller
ask the injector to instantiate the class, it can satisfy all of the dependencies of MyController
without the controller ever knowing about the injector. This is the best outcome. The application code simply declares the dependencies it needs, without having to deal with the injector. This setup does not break the Law of Demeter.
See also
- Architecture description language
- Factory pattern
- Inversion of control
- Plug-in (computing)
- Strategy pattern
- AngularJS
References
- ↑ Niko Schwarz, Mircea Lungu, Oscar Nierstrasz, “Seuss: Decoupling responsibilities from static methods for fine-grained configurability”, Journal of Object Technology, Volume 11, no. 1 (April 2012), pp. 3:1-23
- ↑ Mark Seemann. Dependency Injection .NET. Manning Publications Company, 2011, p. 4.
- ↑ 3.0 3.1 "The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 330". Jcp.org. Retrieved 2013-12-11.
- ↑ "The Dependency Injection Design Pattern".
- ↑ Proffitt, Jacob. "Dependency Injection".
- ↑ Marston, Tony. "Dependency Injection is Evil".
- ↑ Martin Fowler (2004-01-23). "Inversion of Control Containers and the Dependency Injection pattern - Forms of Dependency Injection". Martinfowler.com. Retrieved 2014-03-22.
- ↑ "Yan - Dependency Injection Types". Yan.codehaus.org. Retrieved 2013-12-11.
External links
Wikimedia Commons has media related to Dependency injection. |
- A beginners guide to Dependency Injection
- 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
- Martin Fowler's original article that introduced the term Dependency Injection
- P of EAA: Plugin
- The Rich Engineering Heritage Behind Dependency Injection - Andrew McVeigh - A detailed history of dependency injection.
- What is Dependency Injection? - An alternative explanation - Jakob Jenkov
- Writing More Testable Code with Dependency Injection -- Developer.com, October 2006
- Managed Extensibility Framework Overview -- MSDN
- Old fashioned description of the Dependency Mechanism by Hunt 1998
- Refactor Your Way to a Dependency Injection Container
|
Nagendra