services.js

1.0.0 • Public • Published

Services.js

Build Status Semver npm version Dependencies Development Dependencies

Services.js is an opinionated Dependency Injection Container for JavaScript using explicit service declarations. It is designed to work inside browsers (ran through a build tool like browserify or webpack) as well as in nodejs based environments.

Getting Started (tl;dr)

Using Services.js is quite easy. Install services.js using npm and load the dependency using require or utilizing your browser build tool of choice:

    npm install --save services.js
    var Services = require("services.js");

    var builder = new Services.Builder();
    var container = builder.build([servicesConfig, ...]);
    
    var app = container.get("application");

    app.run();

Once your required the library you need to instantiate a new Builder, which will parse and handle your service definitions given to the Builder#build method. Once finished you will get back a fully configured dependency injection container, which you might use to query for any dependency. Usually this will be the entry point of your application. Once you have that just kick of this entry point and the container will already have taken care of correctly wiring up all your different components fitting them into each other based on your configuration, which look something like this:

{
    "services": {
        "application": {
            "class": require("my_application_class"),
            "arguments": [
                "serviceA",
                "serviceB"
            ],
        },
        "serviceA": {
            "class": require("my_service_a"),
            "arguments": [
                "someStaticValue"
            ]
        },
        "serviceB" {
            "factory": function(container) {
                //... some weird stuff needed to construct serviceB
            }
        },
        "someStaticValue": {
            "value": "Some static value, might be a string or an object, or whatever ;)"
        }
    }
}

This little example should provide you with the needed information to get going. At least you should be able to realize if Services.js is the probably the right tool for the job, or specifically for your problem. For details take a look at the other sections of this document.

Why another IoC container?

Services.js has a different approach then most DICs available for JavaScript. It does not try to automatically extract dependencies using constructor parameters (even though this could be implemented for it quite easily). Neither does it use dependent service declarations on the services itself, like for example the famous angularjs DIC does.

It uses a different approach, which has been inspired by frameworks like Symfony; one or more dedicated service declaration files. This allows to not only inject dependencies by name, but to easily inject different services with the same interface in different situations.

Why would you want to do that?

An example says more than thousand words. Therefore imagine the following scenario:

You have two different views onto the same data. One of your views needs to always show the last 3 entries of some information, while the needs to show the last 500 entries of the same information. The 3 entry view needs to be highly accurate, while the other view needs less accuracy and may be out of date a little. Therefore you have two implementations of your DataGateway, one which provides most recent entries, while the other implements intelligent caching to be faster and less harmful for your servers. If you would have annotated both of your views (or Stores, or whatever layer you might put between that) to just use a DataGateway your DIC would not know that each of them needs a different one. You would have to name the dependencies differently, like CachedDataGateway and DirectDataGateway. However, what if you later on were able to optimize your storage layer, or might have overestimated the load, which the accurate data polling has. To switch your big view from the CachedDataGateway to the DirectDataGateway you would need to edit this view in order to change the dependency. But one of the whole points of IoC is that you don't need to do that in order wire your components differently. An external services file helps you fix this problem easily.

class LittleView {
    constructor(DataGateway) {
        ...
    }
}

class BigView {
    constructor(DataGateway) {
        ...
    }
}

Both your views just specify they want something resembling the DataGateway interface. Next is your services.json

{
    "services": {
        "DirectDataGateway": {
            "class": ...
            ...
        },

        "CachedDataGateway": {
            "class": ...
            ...
        },

        "BigView": {
            "class": ...
            "arguments": [
                "CachedDataGateway"
            ]
        },

        "LittleView": {
            "class": ...
            "arguments": [
                "DirectDataGateway"
            ]
        }
    }
}

The external definition allows for easily swapping out dependencies for each other as long as they have the same interface without touching the different components at all. Besides the usual class based dependencies there are different ones as well like static value or factory methods. See the section about FactoryCreators for more details about the ones shipped as well as rolling your own.

API

Builder

Processing always starts with a Builder. The Builder is the component, which processes your configuration data to create a services Container out of it.

var Services = require("@jakobwesthoff/services.js");

var builder = new Services.Builder();

Builder#construct([factoryCreator, ...]?)

The Builder constructor takes an optional array of FactoryCreator instances, which will be used to create factory functions for all the different services and dependencies defined. If no array is provided the default configuration of FactoryCreators will automatically be used. In most situations this is what you want to do.

var builder = new Services.Builder([
    new Services.ClassFactoryCreator(),
    new Services.FactoryFunctionFactoryCreator(),
    new Services.ValueFactoryCreator(),
    ...
]);

The most common reason to specify the FactoryCreators on your own is to utilize specialized implementations of those you might have done for your project. If you want your own in conjunction with the defaults don't forget to specify those as well. A look into the constructor of Library/Builder.js will tell you which ones are the default.

Builder#build([configuration, ...])

The Builder#build method is the main entry point into the Builder. It basically converts your service definitions into a configured Container, which may then used to retrieve any defined dependency, while ensuring it is correctly wired up.

    var serviceConfiguration = require("services.js");
    var container = builder.build([serviceConfiguration]);

All given configurations will be merged, where each following configuration supersedes the one before it. This allows for easy overrides based on certain environments (eg. Production vs. Development or Server- vs. Client-Side). Details on the configuration syntax itself can be found in the section Service Configuration.

Container

A Container is returned by the Builder#build method. It can not be manually instantiated or otherwise manipulated. It basically is just a processed version of your service configuration, which allows easy and fast access to each correctly wired up dependency.

Container#get(serviceName)

Once a Container instance has been build any service may be retrieved from it using its get method. The needed serviceName is the same one used inside your service definition.

var app = container.get("Application");
app.run();

In most situations your application has one or a small number of entry points. Usually it is enough to retrieve this entry point and simply run it. The service container will take care of instantiating and wiring up all services from this point on according to your configuration.

Service Configuration

Each and every used service needs to be configured inside at least one service configuration. A Service Configuration is a simple JavaScript object structure of the following form:

{
    "services": {
        "serviceA": {
            ...
        },
        "serviceB": {
            ...
        }, 
        ...
    }
}

Each key inside the services object represents and names one service, which further on is configured using a set of key values pairs itself. Which keys are treated inside a service is determined by the FactoryCreator used to build it. FactoryCreators usually are selected based on a specifically named key. The ClassFactoryCreator for example is triggered by the existence of the class key, while the ValueFactoryCreator is triggered by the value key.

Let's assume you want to define a Class as a dependency, it would look like this:

"MyUltraCoolClass": {
    "class": require("some/path/to/my/ultra/cool/class"),
    "arguments": [
        "someServiceA",
        "someServiceB",
        ...
    ],
    "isSingleton": true
}

The class key tells the Builder to utilize the ClassFactoryCreator for the handling of this service, which, as the name suggests, handles classes.

The class may be provided any way: by using require in a corresponding environment, by using the dependency injected by AMD (require.js), by utilizing an import statement (ES6) or by using any other means to get it into the context. The provided structure just needs to be the corresponding contructor function you want to handle as a service.

arguments

Every service may have an arguments key, which contains an array with zero or more entries. Those entries are strings specifying which other services should be injected into this one. The services are injected in the same order they are specified in this array.

Mostly all FactoryCreators handle those arguments, while some might decide to simply ignore them, like the ValueFactoryCreator where further services simply don't make sense.

isSingleton

A lot of FactoryCreators support the isSingleton key, which specifies, if a service should be instantiated anew each time it is needed somewhere, or if the same instance should be used over and over.

Different FactoryCreators have different defaults here. They always assume the most common use case as default. The ClassFactoryCreator for example treats services not as a singleton by default and will instantiate them each time anew, while the FactoryFunctionFactoryCreator treats a factory function as singleton by default and will only call it once, storing its result.

The ValueFactoryCreator ignores this option all together, as it always returns the same static value.

other

Other specialized FactoryCreators may take arbitrary options to provide for proper configurability.

FactoryCreators

FactoryCreator implementations are one of the integral parts of Services.js. Different FactoryCreators allow for different kinds of service definitions. Essentially each implementation parses a single service definition and returns a factory function, which is then used by the configured container to create and manage any requested dependency.

FactoryCreators are constructor functions (classes), which need to provide the following methods:

FactoryCreator#canHandle(definition): Return true or false based on the fact, if the given service definition can be handled by this specific implementation.

FactoryCreator#create(definition): Return an Immutable.Map, which needs to have the following structure:

{
    meta: {
        dependencies: [
            ...
        ]
    },
    factory: function(container) {...}
}

All inner structures are supposed to be Immutable data structures (Lists and Maps). The dependencies list is a simple list of strings containing all the dependencies, which need to be provided by the container, once the service is built. Usually this is the same list of dependencies (arguments) provided in the given definition. The factory function is an arbitrary JavaScript function, which will be called each time the service is needed. It is provided with the configured container and has to return the service in question.

ValueFactoryCreator

The ValueFactoryCreator is used every time a service definition does contain the value key. It ensures that the given value is provided as service, every time it is requested. It does neither handle dependencies nor isSingleton, as there is nothing to handle for a static value.

Values can be of any kind: Boolean, Number, String, Object, Array, Function, ...

"myServiceName": {
    value: {
        my: "static",
        configuration: "object"
    }
}

"myOtherServiceName": {
    value: require("React");
}

ClassFactoryCreator

The ClassFactoryCreator is used in cases, where constructor functions (classes) need to be instantiated to be provided by the container. It is automatically used if a services has a class key. The value for this key is supposed to be the corresponding constructor function.

By default each time the dependency is needed a new instance will be created. The constructor will be called with all the defined arguments in the given order.

If isSingleton is set to true, the class will only be instantiated once and later on reused.

FactoryFunctionFactoryCreator

The FactoryFunctionFactoryCreator is the most flexible, but therefore most configuration intensive FactoryCreator. It is triggered by the existence of the factory key inside the given definition.

The factory value is supposed to be a function. This function will be called each time the requested service is needed. It will be provided with all the specified arguments in the given order. It is supposed to return the fully configured service, which is then to be used. This may of course be any data type.

If no arguments are specified the factory function is given the configured container instance, which allows for highly dynamic services.

By default the factory function is only called once. Its return value is stored and reused every time the service is requested. To always call the factory function, when the service is needed set isSingleton to false.

Package Sidebar

Install

npm i services.js

Weekly Downloads

5

Version

1.0.0

License

MIT

Last publish

Collaborators

  • jakobwesthoff