Dependencies in NodeJS

Static dependencies are well managed by NodeJS but once require is too limiting then try dependency inversion and an inversion of control container.

Static dependencies are easy to manage with require but that tightly couples things together and dynamic run-time dependencies don't fit well with that approach. We can decouple things with dependency inversion but those dependencies still have to be managed somewhere. "Inversion of control container" is a bit of a mouthful (and not the clearest term either) but it can save the day for applications where dependencies are becoming hard to manage.

Limitations of module loading

NodeJS offers us a very natural way to break up and reuse logic, CommonJS modules (and ES6 modules behind a flag). If one module wants to use logic from another module it can use require to get a reference to it:

// --- ReportService.js ---
module.exports = function ReportService () { ... };

// --- index.js ---
const reportService = require('./ReportService');
// TODO: Do something amazing with `reportService`

Now, if you compose your whole application from pure functions then this is the end of the story; you can unit test all and any functions exposed by your modules and your entry point will compose them then supply any state and coordinate any side effects.

On the other hand, when you introduce state into any of your modules (for example, using an OO style with classes) require becomes quite limiting.

The problem is coupling

When a module requires another module then it tightly couples them and a NodeJS module is effectively a singleton since the returned object will be cached and returned for future require calls that resolve to the same file path (spec).

When you trace the web of require calls you are effectively exploring one module that's been split up. That's usually better than having one huge module but is it really your intent that your entire codebase (and all of it's third party dependencies) is conceptually just one thing?!

As another example, pretend you want to create a module but don't have its dependencies finished yet. For example, you might want to make something that depends on UserService without worrying about how it is implemented.

// --- ReportService.js ---
const userService = require('./UserService');

module.exports = function ReportService () {
    return {
        someOperation: () => {
            // Use `userService` in some way
        },
    };
}

// --- UserService.js ---
// Errr...
// I didn't want to have to think about this yet...

In that situation having a static dependency forces you to create a module and ties you to exactly that module. Yes, you can vary the implementation inside UserService module, and yes proxyquire can allow you to reach in and monkey-patch NodeJS into using a fake for testing but can't we do better?

Dependency inversion is a solution

In the OO style, you can invert the dependency and expose a factory that accepts the dependencies then makes those available for the various members of the returned object (an example of dependency injection as a way to get the benefits of dependency inversion):

// --- ReportService.js ---
// NOTE: No `require` statements
module.exports = function ReportService (userService) {
    return {
        someOperation: () => {
            // Use `userService` in some way
        },
    };
}

This isn't a free lunch. By inverting the dependency from ReportService to UserService we have made our code easier to test and to maintain but something somewhere still needs to know about that dependency and supply ReportService with it. You can go quite a long way with manually doing exactly that:

// --- index.js ---
const userService = require('./HardCodedUserService');
const ReportService = require('./ReportService');

// Create a `reportService` instance with its dependencies
const reportService = ReportServiceFactory(userService);
// ...

However, if you follow this road then you can all too easily end up with the maintenance problem of having this creation pattern, and all the attendant knowledge of dependencies, scattered throughout your application. So, static dependencies weren't great, and all we've done is swap that for a different type of maintenance problem! Woe is me!

Inversion of control containers to the rescue

Wouldn't it be nice if something could manage all these dependencies for us so we could just ask for a ReportService and it could figure out the details? Enter inversion of control (IoC) containers and the composition root:

// --- index.js ---
// Register things with the container (the composition root)
const container = require('./IoC');
// NOTE: Assume it automagically detects dependencies
container.register('userService', require('./HardCodedUserService'));
container.register('reportService', require('./ReportService'));

// Ask the container for things
const reportService = container.resolve('reportService');

In order for container to supply you with a ready to go ReportService it has to somehow know about and satisfy any dependencies. Discussion of the different approaches to that are not relevant here, suffice it to say that various containers solve this for you (see further reading for an example).

With that final piece in place we can write loosely coupled modules that are easy to test and cheaper to maintain.

Further reading

Published on: 16 Nov 2017