Dependency injection is a set of software design principles and patterns that enables us to develop loosely coupled code, it is also one of the most misunderstood concepts in the object oriented programming.
The Dependency injection pattern is strictly related to SOLID principles, for more information: SOLID principles using Typescript
The main purpose of the DI is the maintainability through the loose coupling technique, here is some benefits gained form the loose coupling:
Services can be swapped with other services, late binding refers to the ability to replace parts of an application without recompiling code. For example, the following case allows developers to refers the interface IDataProvider and switch between an relational database and a NoSQL database without change the code:
Extensibility and Maintainability
Code can be extended and reused, and the responsibility of each class becomes clearly defined. Finally, adding a new features is more easy because the structure of code allows developers to understand where apply changes.
DI is an important part of supporting unit testing. Unit tests provide a rapid feedback on the state of an application, but it is only possible to write unit test when the unit can be properly isolated from it’s dependencies.
By using DI, developers have the capability to inject dependencies and mocks inside the application code to test an specific part of application without affecting others.
Here is a simple example of mocking for unit testing:
Dependency Injection containers
DI container is a software library that can automate many of the tasks involved in composing object and managing their lifetimes.
The DI containers usually expose a
Resolve method: it resolves instances of arbitrary types. The DI containers know how to compose the requested type by using a sort of configuration or registration point where you map the abstractions(or interfaces) to concrete types. Finally, DI containers inherently compose object graphs by combining their own configuration with the information from classes constructors.
DI containers configuration
There are 3 ways to configure DI containers:
- Static file as configuration: JSON or XML store informations about the DI mapping. The advantage is that it supports replacement without recompiling, the disadvantage is the no compile-time check;
- Code as configuration: the DI mapping is specified inside the code. The advantage is the compile-time check, the disadvantage is that it not support the replacement without compiling code;
- Auto-registration: a set of rules (or naming convention) locates suitable components and build the mappings. The advantage is that supports replacement without recompilation and enforce conventions to make a code base more consistent. The disadvantage is that there is not compile-time check;
DI container patterns
There are some patterns that describe the right way to use DI containers:
- Composition root: the mapping between concrete classes and interface must be composed as close as possible to the application entry point. The Composition root should be an unique location in the application;
- Register, Resolve, Release: The Register, Resolve and Release pattern describes how to use a DI container. The Register Resolve Release pattern states that a DI Container’s methods must be invoked in this strict sequence. The
Registermethod register components with container. The
Resolvemethod resolves the concrete class basing on an interface. Finally,
Releasemethod destroys the instances when they are no longer needed.
Dependency injection patterns
The Dependency injection patterns describe different ways to implement the Dependency injection inside our application. The most important is the Constructor injection pattern.
The class that uses the dependency exposes a public constructor that takes an instance of the required dependency as a constructor argument. It is good practice to mark the field holding the dependency as readonly, this guarantees that once the initialization logic of the constructor has executed: the field can’t be modified. Here is an example of Constructor injection:
Constructor injection should be your default choice for DI. It addresses the most common scenario where a class requires one or more dependencies, and no reasonable local defaults are available.
Property injection pattern
When a dependency is optional we can expose a writable property that allows a client to supply a different implementation of the class’s dependency than the default.
Property injection should only be used when the class you’re developing has a good local default and you still want to enable callers to provide different implementations of the class’s dependency.
The caller supplies the dependency as a method parameter in each method call, for example:
Method injection is best used when the dependency can vary with each method call. This can be the case when the dependency itself represents a value, but is often seen when the caller wishes to provide the consumer with information about the context in which the operation is being invoked.
Dependency injection is the best way to enable loose coupling, an important part of maintainable code. The benefits we can see from loose coupling are not always immediately apparent, but they become visible over time during the maintenance phase.