Skip to content

Instantly share code, notes, and snippets.

@domenic
Last active September 30, 2021 15:43
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save domenic/5753428 to your computer and use it in GitHub Desktop.
Save domenic/5753428 to your computer and use it in GitHub Desktop.
How DI container config should work (in JS)
"use strict";
// Domenic needs a Tweeter
function Domenic(tweeter) {
this.tweeter = tweeter;
}
Domenic.inject = ["tweeter"];
Domenic.prototype.doSomethingCool = function () {
return this.tweeter.tweet("Did something cool!");
};
module.exports = Domenic;
"use strict";
// Merick also needs a tweeter
function Merrick(tweeter) {
this.tweeter = tweeter;
}
Merrick.inject = ["tweeter"];
Merrick.prototype.doSomethingAwesome = function () {
return this.tweeter.tweet("Did something awesome!");
};
module.exports = Merrick;
"use strict";
function App(domenic, merrick) {
this.domenic = domenic;
this.merrick = merrick;
}
App.inject = ["domenic", "merrick"];
App.prototype.run = function () {
this.domenic.doSomethingCool().done();
this.merrick.doSomethingAwesome().done();
};
module.exports = App;
"use strict";
var diContainer = require("di-container");
// Declaratively wire up dependencies. Note that while Domenic and Merrick both need the "tweeter"
// abstraction, we can choose *via configuration* to give them a different "tweeter" concretion.
// They are completely decoupled from this knowledge.
diContainer.config({
"app": require("./3-App"),
"domenic": {
constructor: require("./1-Domenic"),
inject: {
"tweeter": require("./LolSpeakTweeter-not-shown")
}
},
"merrick": {
constructor: require("./2-Merrick"),
inject: {
"tweeter": require("./LeetSpeekTweeter-not-shown")
}
}
});
// Construct the entire object graph, using above declarative config.
// This is the *only* time you should ever use `diContainer.get`.
diContainer.get("app").run();
// Your framework might use `diContainer.get` itself, e.g. for convention-based lookups.
// But you never should.
// Should Tweet:
// - "LOL I HAZ DID SOMETHING COOL LOL!"
// - "1 d1d s0m3th1ng 4w3s0m3!"
"use strict";
// You can also of course wire up the graph manually.
// That's still doing dependency injection.
var Domenic = require("./1-Domenic");
var Merrick = require("./2-Merrick");
var App = require("./3-App");
var LolSpeakTweeter = require("./LolSpeakTweeter-not-shown");
var LeetSpeekTweeter = require("./LeetSpeekTweeter-not-shown");
var app = new App(
new Domenic(new LolSpeakTweeter()),
new Merrick(new LeetSpeakTweeter())
);
app.run();
@jmreidy
Copy link

jmreidy commented Jun 14, 2013

Do you know of any JS DI frameworks that work in this fashion?

Everything I've found just inspects the constructor arguments, and pulls out dependency mappings from argument names. That's all well and good, except for client-side code that you're obfuscating / minifying.

Your example here uses an explicit Fn.inject mapping, as does Angular's implementation. (Angular's implementation would probably be fantastic if it were generalized into its own lib.)

@domenic
Copy link
Author

domenic commented Jun 19, 2013

@jmreidy I think Intravenous might fit the bill. We tried using it on a project once that was just using "raw DI" (i.e. manually compose everything in the composition root). We almost made it work, but retrofitting the auto-factory stuff onto what we were doing didn't end up going so well, and then I left the company, so, welp.

@gjohnson
Copy link

gjohnson commented Jul 5, 2013

@jmreidy might want to check out node-di too, think it's written by one of the angular guys.

@domenic How do you decide what to inject and what to leverage the module system for (via require or import)? I struggle with this because it seems like using 100% DI might not make sense at times and there might be a rule of thumb to go by.

For example, assume we have a module that needs a db client, mocking is not a big deal because we want our tests to actually run against our db instance, so let's not use testability as a reason to favor one over the other...

We could do it with no DI and leverage our trusty module system:

var db = require('db');

function Foo(){
  this.client = db.createClient();
}

module.exports = Foo;
var Foo = require('./foo');

var foo = new Foo();

or

We could leave it open for DI:

function Bar(client){
  this.client = client;
}

module.exports = Bar;
var db = require('db');
var Bar = require('./bar');

var bar = new Bar(db.createClient());

The latter of the two is clean and maybe once we throw db client configuration options into the mix it starts to make more sense to favor it. However, sometimes I feel like DI in these cases can be a leaky abstraction, meaning consumers of Bar will need to know how it works internally. For example, perhaps Bar will call a blocking operation on the client or close the connection on the client, now the consumer needs to know to pass Bar it's own client instance instead of one that is perhaps being shared elsewhere in our application.

Any thoughts/advice/whatever would be much appreciated!

@royriojas
Copy link

I really believe that if you're using CommonJS modules already (with Browserify for example) you don't really need a DI container.

In order to prove it, I have rewritten the Coffee Maker example found in this video https://www.youtube.com/watch?v=_OGGsf1ZXMs#t=121

https://github.com/royriojas/coffe-maker

Basically you can just use the injectr approach of provide a second parameter to the require function.
This second parameter is the list of modules to be mocked when required inside the test. This means require is tampered in your testing environment.

So using the same file as the one you provided like:

var db = require('db');

function Foo(){
  this.client = db.createClient();
}

module.exports = Foo;

In testing env

var mockDB = {};
var Foo = require('./foo', { 
   'db': {
     createClient: function () { 
       return mockDB; // mocked client object; 
     }
  } 
})
var foo = new Foo();
expect(foo.client).to.equal(mockDB);

And that's it, now you can write your tests with ease.

I know for sure that DI can do way more things that just replace a mock during testing, but I would say that the vast majority of javascript projects just need to reuse the code during testing. So this will work.

Using this approach I was also able to switch implementations on runtime, that is a bit more complicated and involves playing with browserify to require/export modules, to make them available outside the bundle, but it is totally possible. So far I have not found a single feature provided by a DI container to make me thing we need one in Javascript.

I have provided a demo of how to do this with Karma and publish the code for be consumed in a browser.

I know also that ES6 modules are coming, I would rather prefer CommonJS format, but let's see what happens. It is good to have more options...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment