1.3.0 • Published 4 years ago

ja-container v1.3.0

Weekly downloads
-
License
MIT
Repository
gitlab
Last release
4 years ago

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:

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. There absolutely must be a way to resolve the dependency into a target class manually, without use of the container
  9. The depdendency resolver should not be overloaded with unnecessary logic. The less it does to suffice the requirements, the better.
  10. 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,
            }),
        },
    }),
1.3.0

4 years ago

1.2.0

4 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago