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.
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:
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:
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
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
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
export default const SomeService = 'SomeService';
We'll create MyService
as the Concrete service and bind it into the container with the abstract key SomeService
export default class MyService {
}
Now, we instruct the Service Container that MyService
is a concrete of the key SomeService
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
.
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 👍
Last updated
Was this helpful?