Service Container

@fusion.io/container

Fusion Framework was designed with Dependency Injection (DI) in mind. To archive a seamlessly DI, the heart of Fusion Framework is a service container.

Dependency Injection Introduction

In short, Dependency Injection (DI) is a technique whereby passing or injecting objects (the dependencies/services) to another object via constructor or getter methods. Using DI properly, your application will be easier to test and better code reuse more about DI.

For example, a booking service will find a ticket for an user, mark it as booked and send an email to the user about the ticket.

BookingService.js
class BookingService {

    constructor(ticketStore, emailService) {
        this.ticketStore  = ticketStore;
        this.emailService = emailService;
    }
    
    async book(user) {
        const foundTicket = await this.ticketStore.find();
                
        await this.ticketStore.booked(foundTicket, user);
        
        await this.emailService.send(ticket, user.getEmail());
    }
}

In the above example, we have injected 2 services into the BookService. ticketStore for finding an available ticket, and emailService for sending the ticket to the user.

To use this BookingService, we have to initialize the ticketStore and emailService first:

main.js
const bookingService = new BookingService(
    new TicketStore(), 
    new EmailService()
);

// ...

await bookingService.book(user);

The trade-off of DI is the complexity of initialization steps. Especially when the services are using in a nested form. For example, the ticketStore might need a DatabaseConnection to perform SQL query and emailService might need an SMPT protocol for sending the email:

main.js
const bookingService = new BookingService(
    new TicketStore(new DatabaseConnection()), 
    new EmailService(new SMTP())
);

// ...

await bookingService.book(user);

To create an instance of BookingService, we first need to know how to create a TicketStore service, which in turn needs to know how to create a DatabaseConnection and so on... In real world application, the source code is even more complicated.

The Service Container of Fusion Framework was created to address this problem.

Service Container has another name called Inversion of Control Container or IoC Container.

Fusion Service Container was inspired by Laravel's Service Container.

Service Container

A service container is a simple map object for holding and getting dependencies/services. To use the Service Container, we need to install it from NPM

npm install @fusion.io/container

Binding basics

To bind a service into Service Container, we'll use the .bind() method:

import { container } from '@fusion.io/container';
import HelloService from './HelloService';

container.bind('helloService', () => new HelloService());

The .bind() method requires 2 parameters: the dependency key helloService and a function returning an instance of the service - we call it as a Factory Function.

After we bind a service, we can retrieve it from the container by calling the .make() method:

const helloService = container.make('helloService');

console.log(helloService.sayHello()); // Hello World

We can also call the .make() method inside the Factory Function. By doing so we can instruct the container how to make a service with nested dependencies easily:

container.bind('fooService', () => new FooService());
container.bind('barService', () => new BarService(container.make('fooService')));
container.bind('fooBarService', () => new FooBarService(container.make('barService')));

const fooBarService = container.make('fooBarService');

In the above example, we have a fooBarService is using the barService as a dependency. And the barService is using fooService as a dependency for itself.

Singleton binding

If your service was bounded to the to the container via .bind() . Every time we call the .make() method, the container will invoke the Factory Function again to get your service instance.

container.bind('fooService', () => new FooService());

const instance1 = container.make('fooService');
const instance2 = container.make('fooService');

console.log(instance1 === instance2); // false

Sometimes, we only need one instance of service or a singleton, and every time we calling make(), we can get back the same instance. We can archive this by using the singleton() method instead of bind():

container.singleton('fooService', () => new FooService());

const instance1 = container.make('fooService');
const instance2 = container.make('fooService');

console.log(instance1 === instance2); // true

Auto binding

Thanks to ES6 Map, the dependency key is not limited to string value, but any value can be used. Even a class

class FooService {

}

container.bind(FooService, () => new FooService());

Most of the time, you may find your factory function is doing nothing but 2 steps: making dependencies and creating a new service instance with those dependencies:

class Dependency1 {

}

class Dependency2 {

}

class MyService {
    constructor(dependency1, dependency2) {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
    }
    
    // ...
}

container.bind(Dependency1, () => new Dependency1());
container.bind(Dependency2, () => new Dependency2());

// We'll do this most of the time
container.bind(MyService, () => new MyService(
    container.make(Dependency1),
    container.make(Dependency2)
));

So instead of doing this again and again, we support an autoBind() method. The above example can be equivalent to:

class Dependency1 {
    static get dependencies() {
        return [];
    }
}

class Dependency2 {
    static get dependencies() {
        return [];
    }
}

class MyService {
    constructor(dependency1, dependency2) {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
    }
    
    static get dependencies() {
        return [Dependency1, Dependency2];
    }
    
    // ...
}

container.autoBind(Dependency1);
container.autoBind(Dependency2);
container.autoBind(MyService);

Similar to autoBind(), we support autoSingleton().

We also support bind(), singleton() functions as ES7 decorators, so you can shorten the above code to this:

import { bind } from '@fusion.io/container';

@bind()
class Dependency1 {

}

@bind()
class Dependency2 {

}

@bind(Dependency1, Dependency2)
class MyService {
    constructor(dependency1, dependency2) {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
    }
    
    // ...
}

Inversion binding

A best practice to approach DI is binding a Concrete service to the Container with an Abstract key. This will help your source code following the Dependency Inversion Principle. Here is a demonstration about it:

First we'll create an Abstract key which the value was SomeService

SomeService.js
export default const SomeService = 'SomeService';

We'll create MyService as the Concrete service and bind it into the container with the abstract key SomeService

MyService.js
export default class MyService {

}

Now, we instruct the Service Container that MyService is a concrete of the key SomeService

main.js
import SomeService from './SomeService';
import MyService   from './MyService';

container.bind(SomeService, () => new MyService());

// ...

By doing so, the consumer code which using the service as dependecy does not need to be aware about the Concrete service - MyService, but the Abstract key - SomeService.

Consumer.js
import SomeService from './SomeService';

@bind(SomeService)
export default class Consumer {
    constructor(someService) {
        // Here we'll got MyService instance
        this.someService = someService;
    }
    
    // ...
}

Now, in the main.js we can have the final result like this:

import SomeService from './SomeService';
import MyService   from './MyService';
import Consumer    from './Consumer';

container.bind(SomeService, () => new MyService());

const consumer = container.make(Consumer);

// working with the consumer

Method Injection

Sometimes, using constructor injection is overhead. You may find your self using just some dependencies in your class method. We can use the inject() decorator for such situation:

import { inject } from '@fusion.io/container';

class MyService {
    
    @inject('dependency')
    someMethod(parameter1, parameter2, depdency) {
        //
    }
}

const myService = new Service();

myService.someMehod('foo', 'bar');

When using the .inject() decorator, the dependency you specified will be passed to your class method as the last parameters.

That's all you need to know about DI with @fusion.io/container for now 👍

@fusion.io/container is having zero dependency and can be used in any javascript project.

Last updated

Was this helpful?