docs(di): updated di docs
This commit is contained in:
parent
21e2f3c1db
commit
f5864afdbb
|
@ -1,13 +1,14 @@
|
|||
# DI
|
||||
# Dependency Injection (DI): Documentation
|
||||
|
||||
The DI module/library is a port of [di.js](https://github.com/angular/di.js) (+ the best parts of [di.dart](https://github.com/angular/di.dart)) to ES6+A.
|
||||
This document describes in detail how the DI module works in Angular 2.
|
||||
|
||||
## Core Abstractions
|
||||
|
||||
The library is built on top of the following core abstractions: `Injector`, `Binding`, and `Dependency`.
|
||||
|
||||
* An injector is created from a set of bindings.
|
||||
* An injector resolves dependencies and creates objects.
|
||||
* A binding maps a token to a factory function and a list of dependencies. So a binding defines how to create an object. A binding can be synchronous or asynchronous.
|
||||
* A binding maps a token, such as a string or class, to a factory function and a list of dependencies. So a binding defines how to create an object.
|
||||
* A dependency points to a token and contains extra information on how the object corresponding to that token should be injected.
|
||||
|
||||
```
|
||||
|
@ -24,14 +25,6 @@ The library is built on top of the following core abstractions: `Injector`, `Bin
|
|||
[Token] [Flags]
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### Key and Token
|
||||
|
||||
Any object can be a token. For performance reasons, however, DI does not deal with tokens directly, and, instead, wraps every token into a Key. See the section on "Key" to learn more.
|
||||
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
|
@ -50,13 +43,11 @@ var inj = Injector.resolveAndCreate([
|
|||
var car = inj.get(Car);
|
||||
```
|
||||
|
||||
In this example we create two bindings: one for Car and one for Engine. `@Inject(Engine)` declares that Car depends on Engine.
|
||||
|
||||
|
||||
In this example we create two bindings: one for Car and one for Engine. `@Inject(Engine)` declares a dependency on Engine.
|
||||
|
||||
## Injector
|
||||
|
||||
An injector instantiates objects lazily, only when needed, and then caches them.
|
||||
An injector instantiates objects lazily, only when asked for, and then caches them.
|
||||
|
||||
Compare
|
||||
|
||||
|
@ -68,7 +59,7 @@ with
|
|||
|
||||
```
|
||||
var engine = inj.get(Engine); //instantiates an Engine
|
||||
var car = inj.get(Car); //instantiates a Car
|
||||
var car = inj.get(Car); //instantiates a Car (reuses Engine)
|
||||
```
|
||||
|
||||
and with
|
||||
|
@ -78,19 +69,98 @@ var car = inj.get(Car); //instantiates both an Engine and a Car
|
|||
var engine = inj.get(Engine); //reads the Engine from the cache
|
||||
```
|
||||
|
||||
To avoid bugs make sure the registered objects have side-effect-free constructors. If it is the case, an injector acts like a hash map with all of the registered objects created at once.
|
||||
To avoid bugs make sure the registered objects have side-effect-free constructors. In this case, an injector acts like a hash map, where the order in which the objects got created does not matter.
|
||||
|
||||
|
||||
### Child Injector
|
||||
|
||||
## Child Injectors and Dependencies
|
||||
|
||||
Injectors are hierarchical.
|
||||
|
||||
```
|
||||
var child = injector.resolveAndCreateChild([
|
||||
var parent = Injector.resolveAndCreate([
|
||||
bind(Engine).toClass(TurboEngine)
|
||||
]);
|
||||
var child = parent.resolveAndCreateChild([Car]);
|
||||
|
||||
var car = child.get(Car); // uses the Car binding from the parent injector and Engine from the child injector
|
||||
var car = child.get(Car); // uses the Car binding from the child injector and Engine from the parent injector.
|
||||
```
|
||||
|
||||
Injectors form a tree.
|
||||
|
||||
```
|
||||
GrandParentInjector
|
||||
/ \
|
||||
Parent1Injector Parent2Injector
|
||||
|
|
||||
ChildInjector
|
||||
```
|
||||
|
||||
The dependency resolution algorithm works as follows:
|
||||
|
||||
```
|
||||
// this is pseudocode.
|
||||
var inj = this;
|
||||
while (inj) {
|
||||
if (inj.hasKey(requestedKey)) {
|
||||
return inj.get(requestedKey);
|
||||
} else {
|
||||
inj = inj.parent;
|
||||
}
|
||||
}
|
||||
throw new NoBindingError(requestedKey);
|
||||
```
|
||||
|
||||
So in the following example
|
||||
|
||||
```
|
||||
class Car {
|
||||
constructor(e: Engine){}
|
||||
}
|
||||
```
|
||||
|
||||
DI will start resolving `Engine` in the same injector where the `Car` binding is defined. It will check whether that injector has the `Engine` binding. If it is the case, it will return that instance. If not, the injector will ask its parent whether it has an instance of `Engine`. The process continues until either an instance of `Engine` has been found, or we have reached the root of the injector tree.
|
||||
|
||||
### Constraints
|
||||
|
||||
You can put upper and lower bound constraints on a dependency. For instance, the `@Self` decorator tells DI to look for `Engine` only in the same injector where `Car` is defined. So it will not walk up the tree.
|
||||
|
||||
```
|
||||
class Car {
|
||||
constructor(@Self() e: Engine){}
|
||||
}
|
||||
```
|
||||
|
||||
A more realistic example is having two bindings that have to be provided together (e.g., NgModel and NgRequiredValidator.)
|
||||
|
||||
The `@Host` decorator tells DI to look for `Engine` in this injector, its parent, until it reaches a host (see the section on hosts.)
|
||||
|
||||
```
|
||||
class Car {
|
||||
constructor(@Host() e: Engine){}
|
||||
}
|
||||
```
|
||||
|
||||
The `@SkipSelf` decorator tells DI to look for `Engine` in the whole tree starting from the parent injector.
|
||||
|
||||
```
|
||||
class Car {
|
||||
constructor(@SkipSelf() e: Engine){}
|
||||
}
|
||||
```
|
||||
|
||||
### DI Does Not Walk Down
|
||||
|
||||
|
||||
Dependency resolution only walks up the tree. The following will throw because DI will look for an instance of `Engine` starting from `parent`.
|
||||
|
||||
```
|
||||
var parent = Injector.resolveAndCreate([Car]);
|
||||
var child = injector.resolveAndCreateChild([
|
||||
bind(Engine).toClass(TurboEngine)
|
||||
]);
|
||||
|
||||
parent.get(Car); // will throw NoBindingError
|
||||
```
|
||||
|
||||
|
||||
|
@ -146,227 +216,37 @@ bind("some token").toFactory(someFactory);
|
|||
|
||||
The `someFactory` function does not have to know that it creates an object for `some token`.
|
||||
|
||||
### Resolved Bindings
|
||||
|
||||
### Default Bindings
|
||||
When DI receives `bind(Car).toClass(Car)`, it needs to do a few things before before it can create an instance of `Car`:
|
||||
|
||||
Injector can create binding on the fly if we enable default bindings.
|
||||
- It needs to reflect on `Car` to create a factory function.
|
||||
- It needs to normalize the dependencies (e.g., calculate lower and upper bounds).
|
||||
|
||||
The result of these two operations is a `ResolvedBinding`.
|
||||
|
||||
The `resolveAndCreate` and `resolveAndCreateChild` functions resolve passed-in bindings before creating an injector. But you can resolve bindings yourself using `Injector.resolve([bind(Car).toClass(Car)])`. Creating an injector from pre-resolved bindings is faster, and may be needed for performance sensitive areas.
|
||||
|
||||
You can create an injector using a list of resolved bindings.
|
||||
|
||||
```
|
||||
var inj = Injector.resolveAndCreate([], {defaultBindings: true});
|
||||
var car = inj.get(Car); //this works as if `bind(Car).toClass(Car)` and `bind(Engine).toClass(Engine)` were present.
|
||||
```
|
||||
|
||||
This can be useful in tests, but highly discouraged in production.
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
A dependency can be synchronous, asynchronous, or lazy.
|
||||
|
||||
```
|
||||
class Car {
|
||||
constructor(@Inject(Engine) engine) {} // sync
|
||||
}
|
||||
|
||||
class Car {
|
||||
constructor(engine:Engine) {} // syntax sugar for `constructor(@Inject(Engine) engine:Engine)`
|
||||
}
|
||||
|
||||
class Car {
|
||||
constructor(@InjectPromise(Engine) engine:Promise) {} //async
|
||||
}
|
||||
|
||||
class Car {
|
||||
constructor(@InjectLazy(Engine) engineFactory:Function) {} //lazy
|
||||
}
|
||||
```
|
||||
|
||||
* The type annotation is used by DI only when no @Inject annotations are present.
|
||||
* `InjectPromise` tells DI to inject a promise (see the section on async for more information).
|
||||
* `InjectLazy` enables deferring the instantiation of a dependency by injecting a factory function.
|
||||
|
||||
|
||||
|
||||
## Async
|
||||
|
||||
Asynchronicity makes code hard to understand and unit test. DI provides two mechanisms to help with it: asynchronous bindings and asynchronous dependencies.
|
||||
|
||||
Suppose we have an object that requires some data from the server.
|
||||
|
||||
This is one way to implement it:
|
||||
|
||||
```
|
||||
class UserList {
|
||||
loadUsers() {
|
||||
this.usersLoaded = fetchUsersUsingHttp();
|
||||
this.usersLoaded.then((users) => this.users = users);
|
||||
}
|
||||
}
|
||||
|
||||
class UserController {
|
||||
constructor(ul:UserList){
|
||||
this.ul.usersLoaded.then((_) => someLogic(ul.users));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both the UserList and UserController classes have to deal with asynchronicity. This is not ideal. UserList should only be responsible for dealing with the list of users (e.g., filtering). And UserController should make ui-related decisions based on the list. Neither should be aware of the fact that the list of users comes from the server. In addition, it clutters unit tests with dummy promises that we are forced to provide.
|
||||
|
||||
The DI library supports asynchronous bindings, which can be used to clean up UserList and UserController.
|
||||
|
||||
```
|
||||
class UserList {
|
||||
constructor(users:List){
|
||||
this.users = users;
|
||||
}
|
||||
}
|
||||
|
||||
class UserController {
|
||||
constructor(ul:UserList){
|
||||
}
|
||||
}
|
||||
|
||||
var inj = Injector.resolveAndCreate([
|
||||
bind(UserList).toAsyncFactory(() => fetchUsersUsingHttp().then((u) => new UserList(u))),
|
||||
UserController
|
||||
])
|
||||
|
||||
var uc:Promise = inj.asyncGet(UserController);
|
||||
```
|
||||
|
||||
Both UserList, UserController are now async-free. As a result, they are easy to reason about and unit test. We pushed the async code to the edge of our system, where the initialization happens. The initialization code tends to be declarative and relatively simple. And it should be tested with integration tests, not unit tests.
|
||||
|
||||
Note that asynchronicity have not disappeared. We just pushed out it of services.
|
||||
|
||||
DI also supports asynchronous dependencies, so we can make some of our services responsible for dealing with async.
|
||||
|
||||
```
|
||||
class UserList {
|
||||
constructor(users:List){
|
||||
this.users = users;
|
||||
}
|
||||
}
|
||||
|
||||
class UserController {
|
||||
constructor(@InjectPromise(UserList) ul:Promise){
|
||||
}
|
||||
}
|
||||
|
||||
var inj = Injector.resolveAndCreate([
|
||||
bind(UserList).toAsyncFactory(() => fetchUsersUsingHttp().then((u) => new UserList(u))),
|
||||
UserController
|
||||
])
|
||||
|
||||
var uc = inj.get(UserController);
|
||||
```
|
||||
|
||||
We can get an instance of UserController synchronously. It is possible because we made UserController responsible for dealing with asynchronicity, so the initialization code does not have to.
|
||||
|
||||
|
||||
|
||||
### Cheat Sheet
|
||||
|
||||
#### Sync Binding + Sync Dependency:
|
||||
|
||||
```
|
||||
class UserList {
|
||||
}
|
||||
|
||||
class UserController {
|
||||
constructor(ul:UserList){}
|
||||
}
|
||||
|
||||
var inj = Injector.resolveAndCreate([UserList, UserController]);
|
||||
var ctrl:UserController = inj.get(UserController);
|
||||
```
|
||||
|
||||
#### Sync Binding + Async Dependency:
|
||||
|
||||
```
|
||||
class UserList {
|
||||
}
|
||||
|
||||
class UserController {
|
||||
constructor(@InjectPromise(UserList) ul){}
|
||||
}
|
||||
|
||||
var inj = Injector.resolveAndCreate([UserList, UserController]);
|
||||
var ctrl:UserController = inj.get(UserController);
|
||||
// UserController responsible for dealing with asynchrony.
|
||||
expect(ctrl.ul).toBePromise();
|
||||
```
|
||||
|
||||
#### Async Binding + Sync Dependency:
|
||||
|
||||
```
|
||||
class UserList {
|
||||
}
|
||||
|
||||
class UserController {
|
||||
constructor(ul:UserList){}
|
||||
}
|
||||
|
||||
var inj = Injector.resolveAndCreate([
|
||||
bind(UserList).toAsyncFactory(() => fetchUsersUsingHttp().then((u) => new UserList(u))),
|
||||
UserController
|
||||
]);
|
||||
var ctrlPromise:Promise = inj.asyncGet(UserController);
|
||||
// Caller opts in to dealing with asynchrony.
|
||||
ctrlPromise.then((ctrl) {
|
||||
expect(ctrl).toBeAnInstanceOf(UserController);
|
||||
expect(ctrl.ul).toBeAnInstanceOf(UserList);
|
||||
});
|
||||
// No synchronous provider for UserList, results in a NoBindingError.
|
||||
expect(() => inj.get(UserController)).toThrow(new NoBindingError(...));
|
||||
var listOfResolvingBindings = Injector.resolve([Binding1, Binding2]);
|
||||
var inj = Injector.fromResolvedBindings(listOfResolvingBindings);
|
||||
inj.createChildFromResolvedBindings(listOfResolvedBindings);
|
||||
```
|
||||
|
||||
|
||||
#### Async Binding + Async Dependency:
|
||||
### Transient Dependencies
|
||||
|
||||
```
|
||||
class UserList {
|
||||
}
|
||||
|
||||
class UserController {
|
||||
constructor(@InjectPromise(UserList) ul){}
|
||||
}
|
||||
|
||||
var inj = Injector.resolveAndCreate([
|
||||
bind(UserList).toAsyncFactory(() => fetchUsersUsingHttp().then((u) => new UserList(u))),
|
||||
UserController
|
||||
]);
|
||||
var ctrl = inj.get(UserController);
|
||||
// UserController responsible for dealing with asynchrony.
|
||||
expect(ctrl.ul).toBePromise();
|
||||
|
||||
var ctrlPromise = inj.asyncGet(UserController);
|
||||
ctrlPromise.then((ctrl) {
|
||||
// UserList still provided async.
|
||||
expect(ctrl.ul).toBePromise();
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Everything is Singleton
|
||||
An injector has only one instance created by each registered binding.
|
||||
|
||||
```
|
||||
inj.get(MyClass) === inj.get(MyClass); //always holds
|
||||
```
|
||||
|
||||
This holds even when we try to get the same token synchronously and asynchronously.
|
||||
|
||||
```
|
||||
var p = inj.asyncGet(MyClass);
|
||||
var mc = inj.get(MyClass);
|
||||
p.then((mc2) => mc2 === mc); // always holds
|
||||
```
|
||||
|
||||
### Transient Dependencies
|
||||
|
||||
If we need a transient dependency, something that we want a new instance of every single time, we have two options.
|
||||
|
||||
We can create a child injector:
|
||||
We can create a child injector for each new instance:
|
||||
|
||||
```
|
||||
var child = inj.resolveAndCreateChild([MyClass]);
|
||||
|
@ -385,29 +265,3 @@ var instance1 = factory(), instance2 = factory();
|
|||
// Depends on the implementation of MyClass, but generally holds.
|
||||
expect(instance1).not.toBe(instance2);
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Key
|
||||
|
||||
Most of the time we do not have to deal with keys.
|
||||
|
||||
```
|
||||
var inj = Injector.resolveAndCreate([
|
||||
bind(Engine).toFactory(() => new TurboEngine()) //the passed in token Engine gets mapped to a key
|
||||
]);
|
||||
var engine = inj.get(Engine); //the passed in token Engine gets mapped to a key
|
||||
```
|
||||
|
||||
Now, the same example, but with keys
|
||||
|
||||
```
|
||||
var ENGINE_KEY = Key.get(Engine);
|
||||
|
||||
var inj = Injector.resolveAndCreate([
|
||||
bind(ENGINE_KEY).toFactory(() => new TurboEngine()) // no mapping
|
||||
]);
|
||||
var engine = inj.get(ENGINE_KEY); // no mapping
|
||||
```
|
||||
|
||||
Every key has an id, which we utilize to store bindings and instances. Essentially, `inj.get(ENGINE_KEY)` is an array read, which is very fast.
|
||||
|
|
Loading…
Reference in New Issue