Tramway Dependency Injector is a simple container manager with powerful capabilities to enhance the Tramway core. It includes:
- A new DependencyResolver which acts as a facade to handle all services and parameters
- Dependency injection capabilities which are simple to implement in configuration files
- Environment-aware parameter mapping to harmonize global and environment-specific parameters. Useful for Dockerized projects.
- Container and ContainerManager classes to create and interface with containers of data
- ClassDefinition and ClassBuilder to convert configuration into fully developped classes. Among other utilities. and so much more.
npm install tramway-core-dependency-injector
https://gitlab.com/tramwayjs/tramway-core-dependency-injector-example
In addition to using the new configuration structure detailed below, you will need to add the following to your server configuration file - in Tramway examples this is the server.js
file at the root - to get started.
import {DependencyResolver} from 'tramway-core-dependency-injector';
import * as parameters from './config/parameters';
import services from './config/services';
DependencyResolver.initialize(services, parameters);
In terms of implementation, this and configuration format is all that is needed to use the service but every piece will be documented further for extendability and tweaks as need be.
With the above code implemented, any time you access the DependencyResolver
anywhere in the project, you will have access to its interface and all of the services and parameters made available at initialization.
import {DependencyResolver} from 'tramway-core-dependency-injector';
+ config
+- parameters
++- global
++- env1
++- env2
++- services
- services
The enhanced structure permits you to have multiple config files of any type - though Tramway presently supports js modules - or one that meets the following criteria. Parameters and Services are given their own sub folders and the environments are separated to make it easier to organize.
To use this structure, ensure you have index.js
files that connect the multiple sub-directories.
The resulting objects should be interpreted as a nested key-value store which can easily be converted to a Map
.
The root of the parameter object must have all the available environments with the default one set to "global". From this point, the different keys apart from global would contain environment-specific variables to override the globals.
Using multiple files in different directories can achieve the following root index.js
file.
import * as global from './global';
import * as docker from './docker';
import * as development from './development';
export {global, docker, development};
As a sample, this structure would translate to the following once read by the DependencyResolver
:
{
"global": {
"exampleAPI": {
"host": "localhost",
"port": 8080,
"path": "model",
"respondAsText": false
}
},
"docker": {
"exampleAPI": {
"host": "api",
}
},
...
}
When running on a docker environment set in NODE_ENV
as 'docker' the exampleAPI parameter will have a host of api
instead of localhost
since docker abstracts location logic to service names.
Services are expected to be read as key-value pairs where the key is the service name and the value is the service definition.
A simple configuration could look like the following.
{
"randomclass": {
"class": RandomClass,
"constructor": [1, 2]
},
"exampleapiconnection": {
"class": RestAPIConnection,
"constructor": [{"type": "parameter", "key": "exampleAPI"}]
}
}
Note that the constructor takes an array of arguments. In exampleapiconnection
the constructor takes an object which is a special dependency-related inject which will be covered later.
In your config you may find a service needs a parameter or another service. This purpose is fulfilled simply with the DependencyInjector
by adding a specially formatted object in your corresponding configuration file.
{
type: "parameter|service",
key: "nameofdependencyparameter"
}
The object with a type of parameter or service and the key will be resolved at runtime to be replaced with the corresponding parameter or service.
For example, a service 'ad1' that is dependent 'ad2' which needs parameters from the 'exampleAPI' parameter would have a config snippet that looks like the following:
"ad": {
"class": RandomClass,
"constructor": [{"type": "parameter", "key": "exampleAPI"}]
},
"ad2": {
"class": RandomClass2,
"functions": [
{
"function": "setC",
"args": [{"type": "service", "key": "ad"}]
}
]
}
The above would translate to the following execution call when getting the ad2
service:
let a = DependencyResolver.getService('ad2');
// Will return the equivalent to the following on a singleton basis:
let a = (new RandomClass2()).setC(new RandomClass({
"host": "localhost",
"port": 8080,
"path": "model",
"respondAsText": false
})
);
In the event that you want to override the ParametersManager
and ServicesManager
to add or modify logic to suit your needs, you can achieve so by extending them in your custom classes and overriding the default creation cycle of the DependencyResolver
Example:
In your <Dependency|Service>Manager file:
import {dependencies} from 'tramway-core-dependency-injector';
const {ParametersManager, ServicesManager} = dependencies;
export default class MyParametersManager extends ParametersManager {}
import {DependencyResolver} from 'tramway-core-dependency-injector';
import {MyServicesManager, MyParametersManager} from './core/dependency_injection/managers';
import * as parameters from './config/parameters';
import services from './config/services';
DependencyResolver.create(new MyServicesManager(), new MyParametersManager()).initialize(services, parameters);
Everything stated above is enough to get you started. If you want to understand how it works or extend from this library, the details will be fleshed out below.
The Dependency Injection service uses the following lifecycle to handle dependencies, services and injections. It follows three simple stages from inception to execution. All created services are stored as singletons in an instances container.
- The
DependencyResolver
gets created inservices.js
with and handles the construction of aDependencyManager
using aServicesManager
and aParametersManager
. - The
DependencyResolver
gets initiatialized with config data.- The parameters get processed first by the
ParametersManager
where a recursive unification is performed to unify environment variables with accordance to that of the current Node process. - A
DependencyInjector
is created to inject parameters into the raw services by means of finding the Dependency Objects. - The
ServicesManager
recursively handles the updated services and stores them in their own container.
- The parameters get processed first by the
- The
DependencyResolver
gets a request to get a service.- It will check an internal instances
Container
and return that instance if it's there. Otherwise, it will create a newDependencyInjector
tasked with getting or building prerequisite services. - The created instance is stored in the instances container handled by the
ServicesManager
and the instance is returned via theDependencyResolver
.
- It will check an internal instances
The DependencyResolver
tucks way the dependency injectio system but was made available for extendability with this library.
Method | Arguments | Purpose |
---|---|---|
create | ServicesManager, ParametersManager | Creates the DependencyManager with the prerequisite sub-managers |
initialize | services: {}, parameters: {} | Converts and stores the configuration data loaded at app startup via the server file |
getService | service: string | Gets a service completely initialized (or an existing instance if it has already been executed) |
getParameter | parameter: string | Gets a parameter completely built with its sub-parameters already built as well |
The container is a simple and secure abstraction of the new Map
class in ES6+. It also is responsible for managing the conversion of Objects to Maps as they get set.
import {container} from 'tramway-core-dependency-injector;
let {Container} = container;
Method | Arguments | Purpose |
---|---|---|
constructor | items: Map/{} | Creates a container with the initial items in a Map or Object (as a key-value relationship) |
buildMap | obj: {} | Converts a given object to a usable map |
get | key: string | Gets an item from the container |
set | key: string, value: * | Sets an item to a key in the container |
An abstract class which makes sure the DependencyManager
, ServicesManager
and ParametersManager
maintain a consistent interface and are compatible with the Container
. It is a bit more restrictive, offering an initialize and getter.
import {container} from 'tramway-core-dependency-injector;
let {ContainerManager} = container;
Method | Arguments | Purpose |
---|---|---|
initialize | obj: {}/Map | Handles initial data |
get | key: string | Gets an item from the container |
The DependencyInjector is an internal facade that is able to communicate with the DependencyManager to bridge missing pieces between the ServicesManager and ParametersManger without directly interfering with their internal processes.
import {dependencies} from 'tramway-core-dependency-injector;
let {DependencyInjectior} = dependencies;
Furthermore, it enforces the structured configuration object for parameters and services which it checks against when scanning for dependencies to replace.
The DependencyManager and its sub-managers ParametersManager and ServicesManager handle all the necessary transactions to register services and make them easily attainable by the DependencyResolver.
import {dependencies} from 'tramway-core-dependency-injector;
let {DependencyManager, ParametersManger, ServicesManager} = dependencies;
import {entities} from 'tramway-core-dependency-injector;
const {ClassDefinition} = entities;
Method | Arguments | Return |
---|---|---|
constructor | key: string, Class: Object, constructorArgs: [], functionsArgs: [] | |
getKey | string | |
getClass | Object | |
getConstructorArgs | [] | |
getFunctionsArgs | [] |
Converts plain javascript objects passed by the DependencyManager
into standardized ClassDefinition
s so they can be used in the builder later on to build new instances.
import {util} from 'tramway-core-dependency-injector;
const {ClassDefinitionFactory} = util;
...
let map = ClassDefinitionFactory.create(services);
Creates a class or service given the corresponding ClassDefinition
. It will construct the instance and make the specified function calls with the arguments and return an instance which will be returned to the DependencyResolver
via the DependencyManager
.
import {util} from 'tramway-core-dependency-injector;
const {ClassBuilder} = util;
...
let service = (new ClassBuilder(dependencyInjector)).build(service);
A static utility made to simplify the process of converting between Objects and Maps recursively.
import {util} from 'tramway-core-dependency-injector;
const {MapFactory} = util;
Method | Arguments | Return |
---|---|---|
create | obj: Object, isRecursive: boolean/false | Map |
convertToObject | value: Map, isRecursive: boolean/false | Object |
The library adds a new ServiceNotFound error which is triggered when a service isn't found. It takes the name of the service as an argument.
import {errors} from 'tramway-core-dependency-injector;
let {ServiceNotFoundError} = errors;
...
throw new ServiceNotFoundError('someservice');