-
Notifications
You must be signed in to change notification settings - Fork 0
Core
The core library provides basic mechanisms that are used by the other packages:
- dependency injection
- aop
- configuration values
- tracing
Dependency is a pattern especially used in large scale applications, in order to increase flexibility, testability and last but mot least the quality and maintainability, by letting a container assemble the different parts of an application - instead or having to write hardwired dependencies - and also taking care of the lifecycle. In my mind a must for every non-trivial application, especially since the coding and runtime overhead is minimal.
In contrast to frameworks that rely on manual registration, this approach relies completely on annotated classes. Let's look at the different possibilities:
Class
Every class annotated with @injectable is eligible for injection.
Example:
@injectable()
class Bar {
const Bar();
}Possible arguments of the annotation are:
-
scope? stringdefines the lifecycle of the instance. Supported scopes are "singleton", "request", "environment". The default issingleton - 'eager?: boolean' if
false- or missing - , the instance will be created on demand, iftruedirectly during the initialization of the containers. - 'module?: string` determines the mapping to modules.
We will come back to the scopes later.
The constructor can define parameters, as long as they are injectable as well.
@injectable()
class Foo {
const Foo(private bar: Bar);
}In this case, the container will construct the objects in the correct order, and call the constructors with the appropriate arguments. The container will of course detect cycles, in which case an exception is thrown.
Every injectable class can define additional methods, that act as factories for additional instances, by annotating methods with @create().
@injectable()
class Factory {
@create()
createBar() : Bar {
return new Bar();
}
}The methods can contain any number of injectable arguments.
The @create() accept the same arguments as @injectable.
It is possible to annotate methods with specific annotations to mark them as callbacks which will be called during construction of the instance. The annotations relate to the lifecycle phases which are:
-
@inject: methods called after constructor instantiation, that provide additional attributes for construction -
@onInit: called after the inject phase, assuming that the object is now completely initialized -
@onRunning: called after all objects have been constructed -
@onDestroy: called after the container has been destroyed
In case of parent classes, all methods except @onDestroy are called from parent to child classes!
All methods can declare any number of injectable parameters.
Example:
@injectable()
class Foo{
const Foo();
@onInit()
onInit(environment: Environment) {
...
}
@onRunning()
void start(manager: ConfigurationManger) {
...
}
@onDestroy()
void stop() {
...
}
}As already mentioned, a couple of methods - including the constructor - can declare any number parameters that are injectable. These are:
- injectable classes
- the type
Environment - annotated parameters that can be resolved.
Currently the annotation @value(key: string, defaultValue?: any) is possible to resolve configuration values.
Check the corresponding chapter.
Classes annotated with @module determine which classes will be handled by a container.
Example:
@module() // implicitely {name: ""}
class ApplicationModule extends Module {
...
}A module is a regular injectable class, so it can add additional factory methods as well.
The logic how classes relate to modules are quite different to a Spring-like logic, since Typescript doesn't remember file or directory locations, so a different approach is taken by matching module names and @injectable module references:
Example:
@module({name: "test"})
class TestModule extends Module {}
@injectable({scope: "test"})
class Test {}TestModule in tghis case will only register Test.
As a default the module name and the @injectable module property is "".
The di container - called environment - is constructed, given a module:
let environment = new Environment({module: TestModule})Different parameters are possible:
-
parent?: Environmentan optional parent environment.
Setting a parent environment means that all of its providers are inherited.
The only method to retrieve objects is
get<T>(type: new(...args: any[]) => T): TDepending on the scope, it will either create a new instance or return a cached instance! Valid scopes are;
-
"singleton"object is created exactly once and cached -
"request"object is recreated on evergetcall. -
"environment"object is recreated per environment,
If a base class is requested, it will return a concrete class, as long as there is only one registered implementing class. Otherwise an exception is thrown.
If an inherited object is requested and created, it will still stay in the defining environment ( the parent in that case ).
The exceptions are objects with scope "environment", which will be recreated even if the parent already owns an instance!