ja-container v1.3.0
Just-A Container
What it is: it is minimalistic Inversion-of-Control container.
There was a story behind
It was given the project with existing sources developed very far much, and the maintainer who got bored of adding and supporting the same boilerplate code to pass state object and common depdencencies everywhere. The project, at the time, didn't have had IoC container. So the maintainer created a task: to provide such IoC container, that would suffice his requirements to free the project completely of passing dependencies boilerplate yet without need to refactor everything. After rewivewing a few existing IoC container packages, the maintainer rejected them by not sufficing his requirements. The requirements lists is the following:
- Using the central container for depdendency injection and without use of more external packages, there should be no more need to add
../../..
to require/import path in modules - To resolve even just one depdencency (an application state, for example) everywhere the code in the target class should be no longer, than existing one - with an argument in constructor
- There should be no need to manually pass container with dependencies into constructors of each class that needs to resolve these dependencies unless this is intended for a specific case
- Resolving a depdendency should not be complex: the standard case of doing
new SomeService(...)
should be automated and the target class should not be forced to call extra methods - The target class should not be forced into any structure, into extending anything or implement any interfaces, and it should be able to state what he needs and where he puts it
- Anything passed into the target class should have no access to list of container contents or to resolvers of dependencies which were not mentioned directly in the target class
- The target class should have no way to modify the resolver of a dependency or any object related to the resolver. The only thing allowed is interaction with the resolved instance
- There absolutely must be a way to resolve the dependency into a target class manually, without use of the container
- The depdendency resolver should not be overloaded with unnecessary logic. The less it does to suffice the requirements, the better.
- The code must be short clean and 100% covered by tests. The tests must check individual methods and the most common use cases.
So this package is what I came up with to suffice these requirements.
Install
npm install ja-container --save
Usage
Central container class
const { Container } = require('ja-container');
const { SomeServiceFromOuterWorld } = require('some-service-from-outer-world');
const SomeService = require('./services/some.js');
const SomeOtherService = require('./services/other.js');
const Something = require('./util/something.js');
const MyContainer = Container({
// SomeSerive will be instantiated as new SomeService(Container) when requsted
SomeService,
// SomeOtherService will be instantiated as new SomeOtherService(Container) when requsted
SomeOtherService,
// Provide function to resolve custom instantiation
SomeServiceFromOuterWorld: () => new SomeServiceFromOuterWorld(),
// Any custom resolver would be provided with Container under the name SomeCustomResolver
SomeCustomResolver: function (container) {
// do whatever you need there
},
// Suppose that Something is a function with > 1 argument (without ...rest)
// it could be bound with container then, and the result would be curried funciton, not an instance
MakeSomething: (container) => Something.bind(null, container),
// Alternative syntax for the same
MakeSomethingAlternative: Something,
// Containers can be nested
AnotherContainer: function ({ SomeService }) {
// If an instance is provided instead of construtor, it would be returned when resolved, not constructed
// This nested container would have no access to other bindings of the outer one
return Container({ SomeService });
},
});
Class that needs some depdendencies
class Dependent {
constructor({SomeService, SomeOtherService, SomeServiceFromOuterWorld}) {
// all of these are resolved instances
this.service = SomeService;
this.otherService = SomeOtherService;
this.outerSerivce = SomeServiceFromOuterWorld;
}
}
Entrypoint
The class Dependent
would need MyContainer
to be passed into constructor. For its dependencies it would be done automatically
const main = new Dependent(MyContainer);
Module and SubModule
Now, lets suppose that you have a structure with many nested modules and you want some modules to have their specific objects inside your container. If you specify these objects as plain objects in the central container, the inside bindings would not be resolved. So, you specify them as a container. Now, you need to resolve something from the base container into the nested one. There comes a Module for that: using Module and SubModule, you will have a base container dragged as a binding so you can both resolve nested bindings and bindings from the base container at the same time:
const { Module } = require('ja-container');
const SomeService = require('./services/some.js');
const MyModule = Module({
// Any bindings to a Container
SomeService,
// Module helper is bound to the Container of each Module
MySubModule: ({ SubModule }) => SubModule({
SomethingNested: ({container: { SomeService }}) => {
// whatever you need
},
}),
});
When you need to define nested Module with a separate container binding, then you just use Module binding for that:
({ Module }) => Module({
SomethingNested: ({ container }) => {
// container there is the container of that level, not parent one
},
}),
Also, using Module/SubModule bindings, if you have separate files for each module, you now don't have to require ja-container
in each module file!
Module options
You can specify options to Module as second argument:
binding (default: 'container'
)
If not falsey value, the top container of the module will be bound to specified key of each container in the module and its submodules. In the following example, it is bound to MyContainerName
:
({ Module }) => Module({
SomethingNested: ({ MyContainerName }) => {
// MyContainerName is a Module bound under name, specified as options.binding
// In all submodules it would be the same
},
}, { binding: 'MyContainerName' }),
self (none by default)
If not falsey value, the current level container of the module will be bound to specified key of each current level container in the module and its submodules. In the following example, it is bound to SelfContainer
:
({ Module }) => Module({
SomethingNested: ({ SelfContainer }) => {
// SelfContainer is a Module container bound under name, specified as options.self
// In all submodules it would be different
},
SomethingElseNested: ({ SubModule }) => SubModule({
SomethingNested: ({ SelfContainer }) => {
// SelfContainer is a SubModule container is bound under name, specified as options.self
},
...
}),
}, { self: 'SelfContainer' }),
methods (default: { Module: true, SubModule: true }
)
Specifies whether to bind methods Module
and SubModule
in the module containers. If a method is falsey value, it won't be bound.
globals (none by default)
Specifies bindings to be bound globally across all submodules of a module. The syntax in the same as module or submodule bindings, and global bindings would be only resolved once. Consider them as an additional Container with specified bindings which is also looked up when looking up for a binding in a container of module or submodule. When resolving bindings in globals, other bindings in globals are available to resolve, but bindings from module or submodule are NOT available
({ Module }) => Module({
SomethingNested: ({ ImportantValue, Util: { Sum } }) => {
// available in top module container
// ...
},
SomethingElseNested: ({ SubModule }) => SubModule({
SomethingNested: ({ SomethingNested, ImportantValue, Util: { Sum } }) => {
// available in submodule container
// ...
},
...
}),
}, {
globals: {
ImportantValue: 123,
Util: ({ SubModule }) => SubModule({
Sum: (_, a, b) => a + b,
}),
},
}),