Dependency Injection in JavaScript 101

Jeremy Likness ⚡️ on January 02, 2019

In my article and presentation "The 3 D's of Modern Web Development" I explain what I believe are critical elements for success in modern JavaScr... [Read Full]
markdown guide
 

Some say that an IoC container isn't necessary in javascript since it already has a module system. I don't exactly agree with those people because when you use a IoC container the functions/classes that you make have a diferent "design". Still, the container does the job of a module system, which make it seem like is a bit redundant.

 

Absolutely. Your point is a good one because JavaScript doesn't adhere to the same rules more strictly typed, object-oriented languages do. Angular chose the approach of injecting dependencies but JavaScript (and TypeScript, for that matter) have better support for aspect-oriented programming in my opinion. To illustrate, for C# if I want a logger the common way to do this is to inject it. I pass my logger instance into the component and then I can swap it out as needed. I can't take a static dependency on Logger.Something because then I've have a hard-coded reliance on Logger. In JavaScript, however, I can just call to Logger.Something because "Logger" is changeable depending on what module I load. I could probably be convinced that in the JavaScript world, we can talk about dependencies strictly by discussing modules and not have to bring IoC/DI into the picture at all unless we're using a framework that takes that approach. Might write another article to expand on that in more detail, thanks for taking the time to respond!

 

Testability is a topic that has always been a bit problematic in the javascript world; mainly because there are a lot of inexperienced programmers using it as their first language and because it has a history of people copy pasting fragments around on web pages.

There are plenty of tools to write tests for javascript but frontend js has this tendency to turn into an untestable mess and there is this misguided concept that it is simply impossible, or worse, redundant. One problem is that people focus on the wrong type of tests and don't understand the difference between unit and integration test. You see people trying to run scenarios against a mock that basically fires up the whole application. This is a lot of work and those tests can be very brittle. I've talked to many engineers that got all excited about the prospect of their test library firing up a headless browser; just so they can test the side effects of events on lambdas on the dom. Don't do that. It's stupid. Make your side effects unit testable and you don't need a browser, or a dom.

Unit testing is where the action is. Unit testing is very straightforward provided your code is testable. This requires inversion of control. Any time that you have code that initializes stuff as a side effect of being used, or imported, you are creating a testability problem. You can no longer consider the unit in isolation since it fires up stuff that fires up more stuff, etc.

Lambda functions make this even worse. Now you have a class with a constructor that constructs other things than itself (simple guideline: constructors must not do work) and then inserts lambdas in those things that do the actual things you need to test. All inside a constructor that gets called from another constructor. Add promises to the mix and you have a perfect horror show of asynchronous side effects of a things being created that may or may not fire events, etc. Simulating all of that in a unit test is hard. This code is effectively hard to test.

Another problem in many javascript projects is the (ab)use of global variables that are accessed from all over the place. Magic elements in the DOM, a library declaring a const with a singleton instance (aka. a global variable) for which the reference is hardcoded everywhere, magic elements in the DOM that contain data that are passed around via imports. These are problems you have to work around when writing a test when you might want to have an alternate value.

The way out is simple: design for testability. IOC is one of several tools you can use and a crucial one but step 0 is acknowledging that what some people claim is elegant is in fact inherently untestable and therefore something needs to change.

 

Thank you for this insightful reply! It makes a lot of sense. I would also add that doing testing the right way (and not just to check a box) transforms the way developers approach code. It's like words: tough for a toddler to communicate with only a few, but an adult can convey very complex thoughts by using the right words as building blocks. Having a test-conscious approach and leveraging patterns like IoC improve your "developer vocabulary" and helps simplify the expression of code.

 

Great post, appreciate the simple explanation.

Just checking, is this line a typo on "piston"?
var piston = $jsInject.get("piston");

Just wondering if it should be "pistons" since that is the label you registered it with or if I'm misunderstanding. Thanks!

 

Great catch! It was a typo and I updated the article. Thanks for the good eyes on this.

 

Jeremy,
Thanks for this post. It actually made me excited like Christmas. I have been trying to work out how and why this is useful and your example clears up all doubts I might have had.
Next, how do I get my team to adopt this pattern? :)

 

I always prefer to lead by example and show the value it provides so they want to adopt it.

 

Incredible! But one doubt: when you'll use ".register", you've to specify first the label and then the class, right?

 

Correct. In the simple DI example you pass a label (string), then an array that should have the function constructor, factory, or instance as the last element.

 

Great! And in this case: "$jsInject.register("engine", ["pistons", Engine]);", you have to pass the "pistons" in the array too because "Engine" receives "pistons" as a parameter, right? And then the last element would be "Engine" cuz is the "current class".

You got it! The important part of the illustration is that "pistons" is just a label, not an implementation, so you have flexibility to define it however you see fit elsewhere.

Oh, I get it! Then I'll use this label on "$jsInject.get()". Amazing!! Thanks ;)

 

Thanks for the post Jeremy 👋

A question I've been mulling over while reading.
Can the terms IoC and DI be used interchangeably?

 

My pleasure! The easiest way for me to summarize it is this:

Inversion of Control is a concept/abstraction and Dependency Injection is a specific implementation of that abstraction.

 

Ah. so IoC is a blueprint and DI is like a house.

Thanks, Jeremy 👊

code of conduct - report abuse