Dependency injection
The dependency injection package is very basic and lightweight. It is primarily meant for internal usage in Banquette
, but you can use it if you only need basic injection mechanics.
So why don't use an external library? There are two main reasons:
- to avoid adding an external dependency
- the end user of
Banquette
(you) may already use a dependency injection library. Chances are it would differ from the one we could have used internally - having control over how the dependency injection works, we can design it to integrate with another library
It is located in the @banquette/dependency-injection
package.
Declare a dependency
You can declare services or modules by creating a class and by decorating it:
import Service from '@banquette/dependency-injection';
@Service()
export class MyService {
}
A service is a singleton. Meaning you will always get the same instance, no matter how many times and where you inject it.
On the other hand, a module will give you a new instance each time you require for it.
You can declare a module with the @Module()
decorator:
import Module from '@banquette/dependency-injection';
@Module()
export class MyModule {
}
Inject a dependency
To inject a dependency, you have two solutions:
1) In the constructor of another object:
import { Inject } from '@banquette/dependency-injection';
import { MyService } from './my-service';
export class ExampleObject {
public constructor(@Inject(MyService) private myService: MyService) {
}
}
The @Inject()
decorator takes the constructor of the class to inject.
2) Using the Injector
class:
import { Injector } from '@banquette/dependency-injection';
import { MyService } from './my-service';
const myService = Injector.Get(MyService);
TIP
You can only register classes in the container. As discussed in the introduction, the injector is very limited voluntarily, to ba as lightweight as possible.
If you need a more robust solution, consider using a full-blown dependency injection library like Inversify,
Banquette
can integrate with it.
Tagged dependencies
In some cases, you may want to inject multiple dependencies in a single inject, while not necessarily knowing what classes will be injected.
You can do this by tagging your modules and services with symbols:
const EncoderSymbol = Symbol('encoder');
@Service(EncoderSymbol)
export class JsonEncoder implements EncoderInterface {
}
@Service(EncoderSymbol)
export class XmlEncoder implements EncoderInterface {
}
You can then inject all of them in one go using the @InjectMultiple
decorator, which takes a symbol instead of a constructor as input:
import { InjectMultiple } from '@banquette/dependency-injection';
export class ExampleObject {
public constructor(@InjectMultiple(EncoderSymbol) private encoders: EncoderInterface[]) {
// encoders will contain: [JsonEncoder, XmlEncoder]
}
}
TIP
You can mix services and modules when injecting multiple dependencies.
Lazy dependencies
If you have two inter-dependent classes, you can't inject them both in each other, because it will create a circular dependency:
a.ts:
import { Inject } from '@banquette/dependency-injection';
import { B } from './b';
@Service()
export class A {
public constructor(@Inject(B) private b: B) {
}
}
b.ts:
import { Inject } from '@banquette/dependency-injection';
import { A } from './a';
@Service()
export class B {
public constructor(@Inject(A) private a: A) {
}
}
import { Injector } from '@banquette/dependency-injection';
import { A } from './a';
// Will fail because A includes B, which includes A, and so on.
const a = Injector.Get(A);
To solve this problem you must inject one of the dependencies lazily. Meaning the dependency will only be resolved when the service is injected:
import { InjectLazy } from '@banquette/dependency-injection';
import { A } from './a';
@Service()
export class B {
public constructor(@InjectLazy(() => A) private a: A) {
}
}
Now it will work.
TIP
Note that @InjectLazy
takes a function as argument, that must return the constructor of the class to inject.
Injecting on properties
Alternatively, you can inject your dependencies on properties:
import { Inject } from '@banquette/dependency-injection';
import { HttpService } from '@banquette/http';
class ExampleObject {
@Inject(HttpService)
public httpService: HttpService;
}
This also works with @InjectLazy
and @InjectMultiple
.
The Injector class
The Injector
class is a static proxy making a bridge between you and the current adapter in use.
It contains the following methods:
Method | Description |
---|---|
Get<T>(identifier: Constructor<T>): T | Gets a dependency from the container. |
GetMultiple<T>(tag: symbol|symbol[]): T[] | Get all the dependencies having at least one of their tag in common with those given as parameter |
Has(identifier: InjectableIdentifier): boolean | Test if a dependency is found in the container. |
GetContainer<T>(): T | Gets the real container class behind the adapter. |
UseAdapter(adapter: InjectorAdapterInterface) | Set the adapter the injector must use (BuiltInAdapter by default). |
Register(metadata: InjectableMetadataInterface) | Register a dependency manually. Prefer using the @Service and @Module decorators instead.If you use a custom adapter and need to register something not supported by the Injector , call GetContainer() and do what you have to do on your custom container instead. |
TIP
Because the Injector
is a proxy, and because all modules and services use it internally (through @Service
and @Module
), you can control where internal dependencies are registered by changing the adapter.
More details are available in the next section.