mainliner
Inversion of control container and dependency injector for node8 and node10 spiced with talent composition.
With mainliner you can create ioc containers. You can register your classes, functions and other things on the container. When you are accessing a class, mainliner is going to create the instance for you. If you defined dependencies for the class they will also be instantiated recursively. If you want you can even do basic composition with the interface of other classes and functions. You can also manage the life cycle of the instances. You can just tell mainliner which instance is supposed to be a generic singleton, a singleton per request perhaps a unique.
The inspiration is the excellent intravenous which seems to be abandoned. All the interfaces of intravenous reimplemented from scratch. With some exceptions...
Differences between mainliner and intravenous
- mainliner supports native classes. Yay!!!
- You cannot dispose anything with mainliner(yet). Boo!!! Hence there are no sub-containers.
- You can do composition with mainliner using talents.
You can register 3 kinds of things on a mainliner container
- class - It will be instantiated and the instance will be returned/injected
- function - It will be executed and whatever it returns will be returned/injected
- passthrough - Anything which is not a class or a function will be returned/injected as it is
##Supported engines
node v8 and v10
Getting started
Basics
To install mainliner:
npm install mainliner
To import mainliner:
const mainliner = require("mainliner");
To create an ioc container:
const container = mainliner.create();
To register a class:
class MyClass {}
container.register("myThing", MyClass);
To get out the instance from the container:
const myThing = container.get("myThing")
To declare dependencies:
class MyClass {
constructor(dependency) {};
}
MyClass.$inject = ["dependency"]
To declare basic composition
class MyClass {
constructor(dependency) {};
}
MyClass.$compose = ["talent"];
A working example (copy/paste it. It will just run.)
const assert = require("assert");
const mainliner = require("mainliner");
class DoesSomeThingForMe {}
function returnsThingForMe() {return "aThing";}
const globalData = {"foo": "bar"};
class MyClass {
constructor(doesSomethingForMe, returnedThing, gd) {
assert.ok(doesSomethingForMe instanceof DoesSomeThingForMe);
assert.equal(returnedThing, "aThing");
assert.equal(gd, globalData);
}
}
MyClass.$inject = ["doesSomethingForMe", "returnedThing", "gd"];
const container = mainliner.create();
container.register("doesSomethingForMe", DoesSomeThingForMe);
container.register("returnedThing", returnsThingForMe);
container.register("gd", globalData);
container.register("myThing", MyClass);
const myThing = container.get("myThing");
assert.ok(myThing instanceof MyClass);
Advanced usage
const assert = require("assert");
const mainliner = require("mainliner");
const globalData = {"foo": "bar"};
class MyClass {
constructor(gd, extraParam1, extraParam2) {
assert.equal(gd, globalData);
assert.equal(extraParam1, "extraParam1");
assert.equal(extraParam2, "extraParam2");
}
}
MyClass.$inject = ["gd"];
const container = mainliner.create();
container.register("gd", globalData);
container.register("myThing", MyClass);
const myThing = container.get("myThing", "extraParam1", "extraParam2");
assert.ok(myThing instanceof MyClass);
Creating instances runtime
You can factorize a class - Only a class - by appending suffix "Factory" to it's registered name. If you do so the injector will inject an object which has a get method which will instantiate the class for you anytime you want
const assert = require("assert");
const mainliner = require("mainliner");
const globalData = {"foo": "bar"};
class DoesSomethingThingForMe {
constructor(gd, param1, param2) {
assert.equal(gd, globalData);
assert.equal(param1, "factoryParam1");
assert.equal(param2, "factoryParam2");
}
}
DoesSomethingThingForMe.$inject = ["globalData"];
class MyClass {
constructor(doesSomethingForMeFactory) {
const instance = doesSomethingForMeFactory.get("factoryParam1", "factoryParam2");
assert.ok(instance instanceof DoesSomethingThingForMe);
}
}
MyClass.$inject = ["doesSomethingForMeFactory"];
const container = mainliner.create();
container.register("globalData", globalData);
container.register("doesSomethingForMe", DoesSomethingThingForMe);
container.register("myThing", MyClass);
const myThing = container.get("myThing");
assert.ok(myThing instanceof MyClass);
Optional dependency
You can declare something as optional if you add the "?" suffix to it's name
const assert = require("assert");
const mainliner = require("mainliner");
const globalData = {"foo": "bar"};
class MyClass {
constructor(gd) {
assert.equal(gd, null);
}
}
MyClass.$inject = ["gd?"];
const container = mainliner.create();
container.register("myThing", MyClass);
const myThing = container.get("myThing");
assert.ok(myThing instanceof MyClass);
Life cycles
You can define the life cycle of an instance (so it works only for classes). There are three life cycles.
- perRequest - The default life cycle of an instance. If you don't specify anything upon registration your instance will be a "perRequest" one. A request means one execution of the "get" method of the container. If the same class is accessed more times upon a request the instance will be created just once. So the same instance will injected multiple times.
- singleton - The instance will be created only once no matter how many requests you make.
- unique - The instance will be created every single time the class is accessed. No matter how many requests you make
perRequest
const assert = require("assert");
const mainliner = require("mainliner");
class OnePerRequest {}
class Class2 {
constructor(onePerRequest) {
assert.ok(onePerRequest instanceof OnePerRequest);
this.refToOnePerRequest = onePerRequest
}
}
Class2.$inject = ["onePerRequest"];
class Class1 {
constructor(dependency1, onePerRequest) {
assert.ok(onePerRequest instanceof OnePerRequest);
dependency1.refToOnePerRequest.extraProp = "extraPop";
assert.equal(onePerRequest.extraProp, "extraPop");
}
}
Class1.$inject = ["dependency1", "onePerRequest"];
const container = mainliner.create();
container.register("onePerRequest", OnePerRequest, "perRequest");
container.register("dependency1", Class2);
container.register("myThing", Class1);
const myThing = container.get("myThing");
assert.ok(myThing instanceof Class1);
singleton
const assert = require("assert");
const mainliner = require("mainliner");
class Singleton {}
class MyClass {
constructor(single) {
assert.ok(single instanceof Singleton);
this.single = single;
}
}
MyClass.$inject = ["single"];
const container = mainliner.create();
container.register("single", Singleton, "singleton");
container.register("myThing", MyClass);
const myThing1 = container.get("myThing");
const myThing2 = container.get("myThing");
assert.ok(myThing1 instanceof MyClass);
assert.ok(myThing2 instanceof MyClass);
myThing1.single.extraProp = "extraProp";
assert.equal(myThing2.single.extraProp, "extraProp");
unique
const assert = require("assert");
const mainliner = require("mainliner");
class Unique {}
class Class2 {
constructor(unique) {
assert.ok(unique instanceof Unique);
this.refToUnique = unique
}
}
Class2.$inject = ["unique"];
class Class1 {
constructor(dependency1, unique) {
assert.ok(unique instanceof Unique);
dependency1.refToUnique.extraProp = "extraPop";
assert.ifError(unique.extraProp);
}
}
Class1.$inject = ["dependency1", "unique"];
const container = mainliner.create();
container.register("unique", Unique, "unique");
container.register("dependency1", Class2);
container.register("myThing", Class1);
const myThing = container.get("myThing");
assert.ok(myThing instanceof Class1);
Composition
You can compose the instance of the class with registered talents. The methods of the talents are gonna be delegated on to the instance itself.
Composition with talents
Conflicts between methods of talents are supposed to be resolved explicitly using either aliasing or excluding. Conflicts between talents and the instance get resolved implicitly which means a talent method overrides an instance method automatically no mather what
const assert = require("assert");
const mainliner = require("mainliner");
const talent1 = mainliner.createTalent({
method1() {}
});
const talent2 = mainliner.createTalent({
method2() {}
});
class MyClass {}
MyClass.$compose = ["talent1", "talent2"];
const container = mainliner.create();
container.register("talent1", talent1);
container.register("talent2", talent2);
container.register("myThing", MyClass);
const myThing = container.get("myThing");
assert.ok(myThing instanceof MyClass);
assert.ok(myThing.method1);
assert.ok(myThing.method2);
Composition when a member is required in the constructor of a class
You can mark a member as required in the constructor of a class. The member need to implemented/provided by a talent
const assert = require("assert");
const mainliner = require("mainliner");
const talent = mainliner.createTalent({
method2() {}
});
class MyClass {
constructor() {
this.method2 = mainliner.required;
}
method1() {
this.method2();
}
}
MyClass.$compose = ["talent"];
const container = mainliner.create();
container.register("talent", talent);
container.register("myThing", MyClass);
const myThing = container.get("myThing");
assert.ok(myThing instanceof MyClass);
assert.ok(myThing.method1);
assert.ok(myThing.method2);
assert.deepEqual(myThing.method2, talent.method2);
Composition when a member is required on the prototype of a class
You can mark a member as required on the prototype of a class. The member need to implemented/provided by a talent
const assert = require("assert");
const mainliner = require("mainliner");
const talent = mainliner.createTalent({
method2() {}
});
class MyClass {
get method2() {
return mainliner.required;
}
method1() {
this.method2();
}
}
MyClass.$compose = ["talent"];
const container = mainliner.create();
container.register("talent", talent);
container.register("myThing", MyClass);
const myThing = container.get("myThing");
assert.ok(myThing instanceof MyClass);
assert.ok(myThing.method1);
assert.ok(myThing.method2);
assert.deepEqual(myThing.method2, talent.method2);
Composition with explicit alias type conflict resolution between talents
You can alias a conflicting method using a special notation in the compose list like: $compose = ["talent: toRename > renamed"]
const assert = require("assert");
const mainliner = require("mainliner");
const talent1 = mainliner.createTalent({
method() {
console.log("talent1 method");
}
});
const talent2 = mainliner.createTalent({
method() {
console.log("talent2 method");
}
});
class MyClass {}
MyClass.$compose = ["talent1: method > renamed", "talent2"];
const container = mainliner.create();
container.register("talent1", talent1);
container.register("talent2", talent2);
container.register("myThing", MyClass);
const myThing = container.get("myThing");
assert.ok(myThing instanceof MyClass);
assert.ok(myThing.method);
assert.deepEqual(myThing.method, talent2.method);
Composition with explicit exclude type conflict resolution between talents
You can alias a conflicting method using a special notation in the compose list like: $compose = ["talent: toRemove -"]
const assert = require("assert");
const mainliner = require("mainliner");
const talent1 = mainliner.createTalent({
method() {
console.log("talent1 method");
}
});
const talent2 = mainliner.createTalent({
method() {
console.log("talent2 method");
}
});
class MyClass {}
MyClass.$compose = ["talent1: method-", "talent2"];
const container = mainliner.create();
container.register("talent1", talent1);
container.register("talent2", talent2);
container.register("myThing", MyClass);
const myThing = container.get("myThing");
assert.ok(myThing instanceof MyClass);
assert.ok(myThing.method);
assert.deepEqual(myThing.method, talent2.method);
Note on testing when you're doing software composition
You might not want to use an ioc container when you're testing your thing. However I still recommend to write test for your composed instances. How? Download the tool wich works under the hood of mainliner and does the composition for you. It's called talentcomposer With this tool you can do the same compositions like you would do with mainliner. See the readme for the details.
Links
Roy Jacobs' intravenous and Mark Seeman's blog
Talents by the Software Composition Group part of the Institute of Computer Science (INF) at the University of Berne
Role orinted programming on Wikipedia