Why you need an IoC container? #4
Replies: 4 comments 4 replies
-
Very nice 👍🏽 |
Beta Was this translation helpful? Give feedback.
-
Great write up! I learned a lot. |
Beta Was this translation helpful? Give feedback.
-
Regarding strings versus symbols for container bindings: I think TypeScript makes strings way more robust and way less "magical." Because strings are treated as unique type literals. I use string identifiers without any worry. So I think this is a great choice. I do wonder though if / how you achieve type safety with the container items. E.g., when you bind Encryptor, does container.resolve("encryptor") know the kind of object it will return? |
Beta Was this translation helpful? Give feedback.
-
Hello @thetutlage, I almost systematically design my applications with an IOC since I use Adonis. In any case, very good "article" if I may say so, that many people should look at with attention ! |
Beta Was this translation helpful? Give feedback.
-
It's usually a good idea to question the existence of an additional layer we are about to introduce in our code and what purpose it serves.
Also, JavaScript/TypeScript does not come with all the bells and whistles; you need to have similar IoC container benefits as PHP or Java. Therefore the list of reasons to use the container is relatively small but compelling.
Terms in use
Offloading construction boilerplate to the "author of the code."
One of the biggest (in fact, the primary benefit) of using the IoC container is to hide the boilerplate for constructing objects from the User of the code.
Following is a simplified version of code from one of my packages (
@adonisjs/drive
).As you can notice, to create an instance of the
LocalDriver
class, we need its config and the router, then Router needs the encryptor, and theEncryptor
needs its config.These dependencies needed to be in a single place to be passed around and construct the final object we want to use.
This might not be a huge problem for smaller applications. However, the framework the size of AdonisJS has a lot of classes relying on each (via Dependency injection), and constructing all the objects by hand can get very messy.
How does the container solves this?
A container is a glorified object which allows you to store dependencies in one place. Here's what the flow looks like in practice:
The
Encryptor
class will register itself inside the container with a unique name (publicly shared).Anyone who needs the
Encryptor
can ask the container to give it by using its name. The container behind the scenes will run the callback passed to thebind
method and returns its value.Similar to the encryptor, all other classes can register themselves within the container.
When you need the local driver, you can call
container.resolve('LocalDriver')
, and everything will be constructed on-demand.Essentially, we have offloaded the responsibility of constructing objects from the user of the code to the author of these individual packages.
How to resolve the config required by container bindings?
In our previous examples of
container.bind
, you can notice that most classes need config to construct class instances.So, how do they get access to this config object? By using convention over configuration.
Let's introduce another class called
Config
, which uses a conventional directory to import all the configuration files and make them available from this class.Next, we can bind it to the container as follows.
config
directory.appRoot
variable is something you need to put into the container when bootstrapping the application manually.As a result, all other classes can now use the
Config
Binding to look up the configuration.The
Encryptor
expects the config to be stored inside theconfig/encryptor.js
file.Isn't using string literals for binding names a wrong choice?
It might appear that using a string value for the container bindings will give rise to all sorts of issues.
Strings are not unique, and two authors can bind a value under the same name, there can be typos when resolving bindings, and so on.
These issues do not exist (at least on a larger scale). The Java Spring IoC container, and PHP PSR standard all recommend and use strings for bindings. Even in AdonisJS, we have used string-based binding names, and no one has ever complained about naming conflicts.
Even then, if you are not convinced, you can use symbols in JavaScript to bind and lookup container bindings. This is because the JavaScript engine treats two symbols even with the same value as unique.
Let's say you are the author of the Encryptor package. You can use the symbols as follows.
I do not use symbols as they make the code visually cluttered (the take is subjective). Since using the container itself is a first step in creating a loosely coupled system, pairing that with string-based binding names should be acceptable.
It's easier to fake and mock implementations
Jest is a popular JavaScript testing framework. It comes with out of the box support for mocking classes (mainly modules). Following is a code snippet from their docs.
./sound-player.js
file with the following contents.The above code works by mutating the modules cache you can access using the
require.cache
property in the CommonJS module system.However, the same cannot work in ESM because one cannot access the tree of imports globally anywhere in the Node runtime. Maybe there is a way to make it all work with loader hooks, so let's wait and see how Jest solves this.
Instead of dabbling between different strategies of mocking for CJS and ESM. We can solve it with a layer of indirection inside the code, and the IoC container perfectly serves that purpose.
In the following example, our
UserService
class usesEncryptor
as a constructor dependency.You can construct it in some other part of the code as follows.
You can bind a fake/mocked implementation to the container during testing. We use the
swap
method in place ofbind
to later restore the original Binding.And voila! It all works without relying on the underlying module system.
Why are IoC containers not in the mainstream in the JavaScript ecosystem?
As a community, we have not been able to decide whether to use semi-colons or not to use them. Therefore, it is expected not to have a consensus on using an IoC container.
As I mentioned earlier, the benefits of an IoC container are limited in a language like JavaScript, and Typescript doesn't improve much upon it.
In languages (to my limited knowledge) like PHP, you can extensively use interfaces for features like reflection and validating that an implementation adheres to an interface at runtime.
So maybe, the limited capabilities of a container are not compelling enough for every sub-community to use it. However, for AdonisJS, it has proved to be working great and helps us write cleaner code without having boilerplate all over the place.
Beta Was this translation helpful? Give feedback.
All reactions