-
Notifications
You must be signed in to change notification settings - Fork 1
Jems DI
Dependency Injection
Inspired in Ninject
An implementation of IoC pattern based on dependency injection that allows you to granulate and decouple your libraries or applications. Wrote using SOLID principles and a variety OOP patterns implementations, also with typescript definitions included.
Why a new dependency injection library for node ?
Actually, I asked myself this question before start to write the code, while it is true that CommonJS work as IoC, it doesn't play well when is time to implement abstractions in our code that sometimes is required for good architectures, also all the DI libraries that I could found out there make me depend 100% in his code and this actually the problem that we wanna solve with a DI library [Dependencies]. Ones do it through decorators, others enforced us to write metadata or extra members in our code making us fully dependent of his code. Also it violate several SOLID principles like Dependency Inversion and Interface Segregation.
Instead of metadata obtained from extra code in our code, it will use the arguments names in the functions to instantiate the dependencies.
Note: It's a known issue that this aproach will not work with ofuscation or a mimification that change the name of function arguments, but this solution is for server side use only, for web you can use an ADM (Asynchronous Module Definition) as require.js.
-
Intall the package.
npm install '@jems/di' --save
-
Instantiate the kernel.
import * as jemsDi from '@jems/di' let kernel = jemsDi.createKernel();
-
Register your dependencies with the kernel fluent API. (Fluent API is optional)
class Lamborghini { getName() { return 'Lamborghini'; } } kernel.bind('car').to(Lamborghini);
-
Use your registered function.
let currentCar = kernel.resolve('car'); console.log('Car Name: ', currentCar.getName());
Output
Car Name: Lamborghini
An it's done.
To handle dependencies lets supose that our lamborghini (let me dream), depends on a car accelerator to move.
import { createKernel } from '@jems/di';
let kernel = createKernel();
class CarAccelerator {
accelerate() {
console.log('Ooh yea.');
}
}
class Lamborghini {
private _carAccelerator: CarAccelerator;
constructor(carAccelerator: CarAccelerator) {
this._carAccelerator = carAccelerator;
}
getName() {
return 'Lamborghini';
}
move() {
this._carAccelerator.accelerate();
}
}
kernel.bind('carAccelerator').to(CarAccelerator);
kernel.bind('car').to(Lamborghini);
let currentCar = kernel.resolve('car');
currentCar.move();
Output
Ooh yea
In order to specify that a dependency is optional or that you want to get a collection as as result of the dependency resolution, you must use alias sufixing.
The available alias sufixing are:
Alias Sufixing | Description |
---|---|
List | A collection of all dependencies for a given alias will be delivered. |
Optional | If there is no dependency registered for a given alias you will get null instead of an error throw. |
Eg.
class Lamborghini {
private _carAccelerator: CarAccelerator[];
constructor(carAcceleratorList: CarAccelerator[]) {
this._carAccelerators = carAcceleratorList;
}
}
class Mazda {
private _carAccelerators: CarAccelerator;
constructor(carAcceleratorOptional: CarAccelerator) {
this._carAccelerator = carAcceleratorOptional;
}
}
With optionals, you may need to ask if the variable is not null before attempt to use it.
The kernel, is used to register, manage and resolve dependencies, also you can use it to creates and administrate containers. We will discuss container in comming examples
.
You need to bind an alias to a type or an object that will be served and delivered whenever the alias be requested in a resolution process.
kernel.bind('car').to(Lamborghini);
You can specify the servicing strategy for the bind, allowing you activate and serve the type or object in different ways depending on your needs.
The available servicing strategies are:
Servicing Strategies | Description |
---|---|
Instance | Serve a new instance of the reference. |
Constant | Serve the reference as is. |
Builder Function | Serve the result of the reference function invocation. |
Custom | Serve what you want to serve. :) |
Eg.
import { createKernel, ServicingStrategy, ResolutionContext } from '@jems/di';
let kernel = createKernel();
class CustomServicingStrategy implements ServicingStrategy {
serve(resolutionContext: ResolutionContext, referenceTarget: any) {
// Just returning the type or object without activation.
return referenceTarget;
}
}
kernel.bind('car').to(Lamborghini).asInstance()
kernel.bind('car').to(ferrari).asConstant()
kernel.bind('car').to(carBuilder).asBuilderFunction()
kernel.bind('car').to(Mercedes).as(new CustomServicingStrategy());
You can specify the delivery strategy for the bind, allowing you deliver the type or object in different ways depending on your needs.
The available delivery strategies are:
Delivery Strategies | Description |
---|---|
Per Call | Deliver by serving a new dependency in each request resolution. |
Per Resolution | Deliver by serving one time per an entire request resolution. |
Containerized | Deliver by serving one time per container. |
Singleton | Deliver by serving just one time. |
Custom | Deliver how you want to deliver :) |
Eg.
import { createKernel, DeliveryStrategy, ResolutionContext, DependencyMetadata } from '@jems/di';
let kernel = createKernel();
class CustomDeliveryStrategy implements DeliveryStrategy {
deliver(resolutionContext: ResolutionContext, dependencyMetadata: DependencyMetadata) {
// Just serving it and returning it.
return dependencyMetadata.servicingStrategy.serve(resolutionContext, dependencyMetadata.activationReference);
}
}
kernel.bind('car').to(Lamborghini).inPerCallMode()
kernel.bind('car').to(Ferrari).inPerResolutionMode()
kernel.bind('car').to(Mercedes).inContainerizedMode()
kernel.bind('car').to(Ford).inSingletonMode()
kernel.bind('car').to(Hyundai).inMode(new CustomDeliveryStrategy())
You can specify validators in order to conditionate when the bind is valid for a resolution request.
The available validator are:
Validator | Description |
---|---|
Ancestor | The dependency is available if inherit from a given type. |
Injected Into Alias | The dependency is available if is injected into a given alias. |
Injected Into Type | The dependency is available if is injected into a given type. |
Injected Exactly Into Alias | The dependency is available if is exactly injected into a given alias. |
Injected Exactly Into Type | The dependency is available if is exactly injected into a given type. |
Custom | The dependency is available if pass your condition :) |
Eg.
import { createKernel, ResolutionContext, DependencyMetadata } from '@jems/di';
let kernel = createKernel();
function customValidator(resolutionContext: ResolutionContext, dependencyMetadata: DependencyMetadata) {
return true;
}
kernel.bind('car').to(Lamborghini).whenAncestorIs(Car)
kernel.bind('car').to(Ferrari).whenInjectedIntoAlias('factory')
kernel.bind('car').to(Mercedes).whenInjectedIntoType(Factory)
kernel.bind('car').to(Ford).whenInjectedExactlyIntoAlias('factoryMachine')
kernel.bind('car').to(Hyundai).whenInjectedExactlyIntoType(FactoryMachine)
kernel.bind('car').to(Toyota).when(customValidator)
You can combine the strategies and validator based on your needs and scenarios.
kernel.bind('car') // Bind car
.to(Lamborghini) // To a Lamborghini class or function
.asInstance() // Serve as an instance
.inPerResolutionMode() // Serve and return the same instance a resolution process
.whenInjectedIntoAlias('alias'); // Only if is injected into de bind alias
The container allow you to issolate data, services, configurations, etc...
Eg.
If you have a multi organizational application you can use containers to separate configurations.
import { createKernel, ResolutionContext, DependencyMetadata } from './di/dist';
let kernel = createKernel();
kernel.createContainer('base');
kernel.createContainer('company1', ['base']);
kernel.createContainer('company2', ['base']);
class Configuration {
public connectionString:string
}
let company1Configuration: Configuration = { connectionString: 'A company 1 great connection string' };
let company2Configuration: Configuration = { connectionString: 'A company 2 great connection string' };
kernel.bind('configuration').inside('company1').to(company1Configuration).asConstant();
kernel.bind('configuration').inside('company2').to(company2Configuration).asConstant();
function onRequest(req, res) {
let configuration: Configuration = kernel.usingContainer(req.param.company).resolve('configuration');
console.log('Lets work with the connection string: ' + configuration.connectionString);
}
This example is raw, to allow you easily understand, but you should do it better than that.
The base container will support the companies containers, so if the companies containers cannot resolve an alias, they will ask the base to resolve it.
If you want to contribute to the project fork the repository and pull request your issues fixes, create issues if something does not go as expected or you got an idea for a new feature.
To set up the project just execute in the root path npm run setup
and the try to build it by executing the command gulp pack-cd
I'm still working on the documentation, it will come soo. I'm working in:
- Guides
- Tutorials
- API documentation
If you need any help implementing the library don't hesitate in write to my mail [email protected]