Dependency injection is one of the most popular patterns in software design. Particularly among developers who adhere to clean code principles. Yet, surprisingly, the differences and relations between the dependency inversion principle, inversion of control, and dependency injection are usually not clearly defined. Therefore, it results in a murky understanding of the subject.
The present blog post aims to clarify the boundaries between these terms. My aim is to present a practical guide on how dependency injection could be implemented and used. By the end of this post, you will know:
- What is the difference between the dependency inversion principle, inversion of control, and dependency injection?
- Why is dependency injection useful?
- What are meta-programming, decorators, and Reflection API?
- How is dependency injection implemented in NestJS?
Software design is a constantly developing discipline. Despite its tremendous practical implications, we still don’t understand how to differentiate between a great and a terrible code of an application. That seems to be a reason to discuss how to characterize great software code.
One of the most popular opinions claims that your code needs to be clean to be good.
The mentioned school of thought is particularly dominant among developers using an object-oriented programming paradigm. Therefore, they propose to decompose a part of reality covered by the application into objects defined by classes. Consequently, group them into layers. Traditionally, we have distinguished three layers:
- User interface (UI);
- Business logic;
- Implementation layer (databases, networking, and so on).
Advocates of clean code believe that these layers are not equal and there is a hierarchy among them. Namely, the implementation and UI layers should depend on the business logic. The dependency injection (DI) is a design pattern that helps to achieve this goal.
Before we move forward, we need to make some clarifications. Although we traditionally distinguish three application layers, their number could be greater. “Higher level” layers are more abstract and distant from the input/output operations.
Let’s meet the dependency inversion principle (DIP)
Clean architecture advocates argue that good software architecture, can be described with the independence of the higher-level layers from the lower-level modules. It is called the dependency inversion principle (DIP). The word “inversion” stems from the inversion of the traditional flow of an application where UI depended on the business logic. In turn, business logic depended on the implementation layer (if three layers were defined). In architecture where software developers adhere to the DIP, the UI and implementation layers depend on the business logic.
A bunch of programming techniques, were invented to achieve DIP. Accordingly, they are referred to together under the umbrella term: inversion of control (IoC). The dependency injection pattern helps to invert the control of the program in object creation. So, IoC is not limited to the DI pattern. Please find below some other use cases of IoC:
- Changing algorithms for a specific procedure on the fly (strategy pattern),
- Updating the object during the runtime (decorator pattern),
- Informing subscribed objects about a state change (observer pattern).
The goals and pros of dependency injection
If you use techniques that invert the flow of the application comes with the burden of writing/using additional components. Unfortunately, it increases the cost of maintainability. Indeed, by extending the code base in this manner, we also increase the system’s complexity. As a result, the entry barrier for new developers is higher. That’s why it is worth discussing the potential benefits of IoC and when it makes sense to use it.
To describe the dependency injection, we separate the initialization of the objects the class uses from the class itself.
In other words, we decouple the class configuration from its concern. The class dependencies are usually called – the services. Meanwhile, the class is described – the client.
I assume the DI pattern is based on putting the initialized services as arguments to the client constructor. Yet, other forms of this pattern exist. For example, setter or interface injection. Nevertheless, I’m describing only the most popular constructor injection.
Overall, DI enables applications to become:
- More resilient to change. Therefore, if the details of the service change, the client code stays the same. So, dependency injects by code and outside of the client.
- More testable. Injected dependencies could be derided. Finally, it results in a decrease in the cost of writing application tests.
When are these benefits worth the developers’ effort? The answer is simple! When the system is supposed to be long-lived and encompasses a large domain, which results in a complex dependencies graph.
Technical frames of implementing Dependency Injection
First, you should consider and understand the pros and cons of the DI. Does your system is large enough to benefit from this pattern? If so, it’s a good moment to think about implementation. This topic will embark us on a meta-programming journey. Thus, we will see how to create a program scanning the code and then performing on its result.
What do we need to create a framework performing the DI? Let’s list some design points that our specification.
- First, we need a place to store the information about the dependencies. This place is usually called the container.
- Then, our system needs an injector. So, we need to initialize the service and inject the dependency into the client.
- Next, we need a scanner. The ability to go through all the system modules and put their dependencies into the container is essential.
- Finally, we will need a way to annotate classes with metadata. It allows the system to identify which classes should be injected and which are going to be the recipient of the injection.
TypeScript, decorators, and reflection API
Specifically, regarding the general points mentioned above, we should focus on the terms of data structures that could help us to implement the specification.
Within the TypeScript realm, we could easily use:
- Reflection API. This API is a collection of tools to handle metadata (e.g., add it, get it, remove it, and so on).
- Decorators. These data structures are functions to run during the code analysis. The API can be used to run the reflection. Then, the classes will get annotated with metadata.
Implementation of dependency injection is NESTJS
That was a long journey! Finally, we collected all the pieces to understand how to implement the dependency injection in one of the most popular NodeJS frameworks – NestJS.
Let’s look at the source code of the framework. Please, check the link: nest/packages/common/decorators/core/. Here, there are two definitions of decorators that interest us.
Both of these files export functions adding metadata to the object. The method is running the defined metadata function from the reflection API.
In the first case (injectable decorator), we are tagging classes as providers. Consequently, they could be injected by the NestJS dependency injection system. The latter case (inject decorator) is different. We are informing the DI system that it needs to provide a particular constructor parameter from the class.
NestJS is grouping providers and other classes into higher architecture groups called: modules. As you probably have foreseen, we use decorators located in nest/packages/common/decorators/modules, as module.decorator.ts.
Similarly to decorators defined above, the meat and potatoes of this code are to export a function that runs reflection API. Also, add meta-data that this class is a module. Inside the module, we need to answer two essential questions.
- Which classes are controllers?
- Which are providers?
Then DI system can know how to instantiate the dependencies.
The NestJS adds the metadata that helps to organize the code into logical units:
– Units of injectable providers.
– Parameters of the constructors needed to inject.
– Modules structuring the dependency graph of the project.
Step by step implementation in NESTJS
How is NestJS using this information? Every NestJS project starts with a more or less similar statement:
const app = await NestFactory.create()
This line runs the create function from the NestFactoryStatic class, which runs the initialize function (among others).
What is the job of the initialize function?
- It creates the dependency scanner, which scans modules for dependencies.
- During the scanForModules, we add modules to the container, which is a Map between a string and a Module.
- Then, scanModulesForDependencies is run, which in essence is running 4 functions: reflectImports, reflectProviders, reflectControllers, and reflectExports.
- These functions have a similar goal: get metadata annotated by the decorators and perform specific actions.
- After, this instanceLoader initializes the dependencies by running the function createInstancesOfDependencies, which creates and loads the proper object.
The described picture is less complex than the complete code of the system. It has to handle edge cases of circular dependencies, exceptions, and so on, yet it conveys the gist of it.
To summarize, we have embarked on a long journey! First, we learned that classes are grouped into layers. Also, they are not equal. To maintain the proper hierarchy among them, we must invert the control. In other words, we need to standardize the flow of the application.
In the case of object creation, we can invert the control by using the dependency injection system. A great example is NestJS. Here, the system works by utilizing decorators and reflection API. Therefore, it enables to transformation metaprogramming in TypeScript.
Briefly all this jazz is worth the effort for complex and long-lived applications.
- Ian Cooper – The clean architecture
- Mark Seeman – Layers, onions, ports and adapters: it is all the same
- Martin Fowler – Inversion of Control Containers and the Dependency Injection pattern
- Martin and Martin – Agile programowanie zwinne
- Konrad Zajda – Inversion of Control – #1 Dependency Injection
- Kai Sassnowski – Demystifying Dependency Injection Containers
- Kamil Myśliwiec – Demystifying Dependency Injection: Angular vs NestJS
- Łukasz Panek – Dependency Injection vs. Dependency Inversion vs. Inversion of Control
- Wikipedia – Dependency Injection – a fine article that surprisingly is a great sum up of Stackoverflow answers.