Inversion of Control in NodeJS
Why and how we built our own IoC library
A little bit of background
For someone with a Java and Spring background, IoC pattern is a great way to organize your application’s code.
For those who don’t have that kind of background, here’s a short introduction to the pattern:
“In software engineering, inversion of control (IoC) is a design principle in which custom-written portions of a computer program receive the flow of control from a generic framework. A software architecture with this design inverts control as compared to traditional procedural programming: in traditional programming, the custom code that expresses the purpose of the program calls into reusable libraries to take care of generic tasks, but with inversion of control, it is the framework that calls into the custom, or task-specific, code.”
The good thing about this library is that it is not a framework itself, but actually a framework builder!
Under the hood
The library works parsing such DSL and adding the components defined to access them globally. This is done in three phases:
Getting into the actual code
The main method we use is
module (line 20), which creates the container where all the components will be located. This method, like the rest of the methods in this code, use the Method Chaining pattern. In this case, it allows us to add the
through the component method. The array of strings passed as parameter enumerates the dependencies of our container.
Once the container is created, we define a new component called
(21) invoking the component method that receives two arguments:
A unique identifier.
The configuration object.
Let’s talk about the second argument.
The object must have a class property so we can know what to instantiate. Basically, with this property we are saying that we’ll add to the container a B object called
Also, and here is one of the big potentials of our lib, this config object could have properties that will end up being attributes of our new component (23). With this, we can assign other components or values to our
In this example,
will have a property called ‘a’ referencing to the
component. The ref method will search in the container for a component called
that must exist. But, we didn’t define the
anywhere. This works because we passed the
dependency to the module method which contains the
These values could be whatever we want, for example, a string, or an integer. We should be careful with this feature because the components are singletons, and it is a good practice to keep the attributes of a singleton object immutable.
The build method (26). Why do we pass the configuration to the
method? Because we can share immutable data between all components. You probably noticed that we never talked about line 24, right? Until now. We pass a config object to the
method because we want to share data between components like the user object. A concrete example could be a database configuration.
Here is when we use our DSL to specify the way that you could access the data. As in the line 24, you define a property like the others (
) and you need to assign the path to the property of the config object that we passed to the build method using String interpolation.
This way, we are saying to nan-ioc to look in the configuration object for a property called
with a nested property called
The build method is the last thing that you need nan-ioc to do before starting to build the container. Here is when all the components are initialized and injected between them.
This method triggers the building phase when the tree is resolved and would fail if a circular dependency was found.
After each component is built, nan-ioc calls
method. This method is not required and will be called only if you defined it. At this point, the component has all its dependencies built.
An example of the usage of this hook is when you need to have all the modules defined and built to start a server.
For us the job is never quite done, so we want to keep improving the module. These are some of the aspects that we want to work on:
Improve error handling: When a circular dependency is detected, an error is thrown. But currently we are not showing exactly which dependency is the wrong one.
Define a component using Annotations: To define a component (with its dependencies) we need to create a separate file that handles that. It would be great if we could define components, factories and dependencies directly in the code using Annotations.
Allow the use of multiple containers: To keep concerns properly separated, we would like to be able to define and use multiple containers. In theory we can, but we haven’t properly tested it.
Define component’s scope: At the building phase we register all the components under the same global scope. We want to add the ability to define different scopes where the components can live.
Directories autoscan: To add any component, we have to manually define the folders where they are. A good enhancement would be scanning automatically each folders in the project.
You can start using the current version of the library from NPM. If you want to add any issues or do participate in the development in any way our GitLab repository is public.
We’ll keep you tuned on the updates. And appreciate all comments and collaborations!