by Ricardo Sousa
These days it’s quite common to use Dependency Injection, which allows a projects’ modules to be loosely coupled. But as projects grow in complexity, we have an astronomical number of dependencies to control.
To work around this problem, we often turn to dependency injection containers. But is it necessary in every situation?
- Without Dependency Injection
- With Dependency Injection
- With Dependency Injection and default parameters
We will structure our example project by feature (by the way — don’t structure your files around roles). So, the structure will be something like this:
├── users/│ ├── users-repository.js│ ├── users.js│ ├── users.spec.js│ ├── index.js├── app.js
Note: For the purpose of this example, we will save the users’ information in memory.
Without Dependency Injection
Analyzing the previous code, we verify that we are limited by the statement:
const usersRepo = require('./users-repository') in users. The users module, with this approach, is strongly coupled to the users-repository.
This limits us to using the implementation of another repository without changing the require statement. When the require is used, we create a static dependency to the required module. With that, we can’t use another repository in the app model besides the one defined by the users-repository module.
Besides that, we are also bound to the users-repository in the users-spec because of the static dependency mentioned previously. These unit tests are for testing only the users module and nothing more. Imagine if the repository was connected to an external database. We would have to interact with the database in order to be able to test.
With Dependency Injection
With Dependency Injection, the users module is no longer coupled to the users-repository module.
The main difference from the previous approach is that now we don’t have the static dependency in the users module (we don’t have the statement:
const usersRepo = require('./users-repository')). Instead of that, the users module exports a factory function with a parameter for the repository. This allows us to pass any repository to the module at a higher level.
Now, in the app module, we have the freedom to define which repository we want to use. Also, look at the unit tests. We can now test the users module without worrying about the repository. Just mock it!
However, let’s be honest — how often do we define dependencies that change throughout the application’s lifecycle? Normally, we try to avoid static dependencies because it makes testing harder. But now, since we want testability, we have to pass an instance of the repository to the users module every time we want to use it.
With Dependency Injection and default parameters
With this strategy, in addition to the Dependency Injection we’ve seen in the previous approach, the parameter defined in the factory function exported by the users module is now a default parameter:
usersRepo = defaultUsersRepo.
With the default parameter, if we don’t pass an argument, the value of the default parameter is used by the function. Otherwise, the argument’s value is used. This is the same as using the Dependency Injection technique defined in the previous approach.
Now, we have the static dependency again in the users module. However, this static dependency is only to define the value used in the default parameter if no argument is passed to the factory function.
With this approach, we are not obligated to pass the repository in the app module when requiring the users module. Still, we can do it. We can also verify that unit tests can continue to use the mock repository, because we are able to pass it instead of using the default parameter’s value.
Feel free to ask me anything.
GitHub repository with the examples: here.
Mattias Petter Johansson has a great Dependency Injection explanation video:
If you enjoyed this article, please give me some claps so more people see it. Thank you!