Skip to content

Commit 2f732c6

Browse files
committed
docs(di): add docs to di module
1 parent 1a7d516 commit 2f732c6

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed

modules/di/docs/di.md

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
# DI
2+
3+
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.
4+
5+
## Core Abstractions
6+
7+
The library is built on top of the following core abstractions: `Injector`, `Binding`, and `Dependency`.
8+
9+
* An injector resolves dependencies and creates objects.
10+
* 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.
11+
* A dependency points to a token and contains extra information on how the object corresponding to that token should be injected.
12+
13+
```
14+
[Injector]
15+
|
16+
|
17+
|*
18+
[Binding]
19+
|----------|-----------------|
20+
| | |*
21+
[Token] [FactoryFn] [Dependency]
22+
|---------|
23+
| |
24+
[Token] [Flags]
25+
```
26+
27+
28+
29+
#### Key and Token
30+
31+
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.
32+
33+
34+
35+
## Example
36+
37+
```
38+
class Engine {
39+
}
40+
41+
class Car {
42+
constructor(@Inject(Engine) engine) {
43+
}
44+
}
45+
46+
var inj = new Injector([
47+
bind(Car).toClass(Car),
48+
bind(Engine).toClass(Engine)
49+
]);
50+
var car = inj.get(Car);
51+
```
52+
53+
In this example we create two bindings: one for Car and one for Engine. `@Inject(Engine)` declares that Car depends on Engine.
54+
55+
56+
57+
## Injector
58+
59+
An injector instantiates objects lazily, only when needed, and then caches them.
60+
61+
Compare
62+
63+
```
64+
var car = inj.get(Car); //instantiates both an Engine and a Car
65+
```
66+
67+
with
68+
69+
```
70+
var engine = inj.get(Engine); //instantiates an Engine
71+
var car = inj.get(Car); //instantiates a Car
72+
```
73+
74+
and with
75+
76+
```
77+
var car = inj.get(Car); //instantiates both an Engine and a Car
78+
var engine = inj.get(Engine); //reads the Engine from the cache
79+
```
80+
81+
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.
82+
83+
84+
### Child Injector
85+
86+
Injectors are hierarchical.
87+
88+
```
89+
var child = injector.createChild([
90+
bind(Engine).toClass(TurboEngine)
91+
]);
92+
93+
var car = child.get(Car); // uses the Car binding from the parent injector and Engine from the child injector
94+
```
95+
96+
97+
## Bindings
98+
99+
You can bind to a class, a value, or a factory
100+
101+
```
102+
var inj = new Injector([
103+
bind(Car).toClass(Car)
104+
bind(Engine).toClass(Engine);
105+
]);
106+
107+
var inj = new Injector([
108+
Car, // syntax sugar for bind(Car).toClass(Car)
109+
Engine
110+
]);
111+
112+
var inj = new Injector([
113+
bind(Car).toValue(new Car(new Engine()))
114+
]);
115+
116+
var inj = new Injector([
117+
bind(Car).toFactory((e) => new Car(e), [Engine]),
118+
bind(Engine).toFactory(() => new Engine())
119+
]);
120+
```
121+
122+
You can bind any token.
123+
124+
```
125+
var inj = new Injector([
126+
bind(Car).toFactory((e) => new Car(), ["engine!"]),
127+
bind("engine!").toClass(Engine);
128+
]);
129+
```
130+
131+
Note that tokens and factory functions are decoupled.
132+
133+
```
134+
bind("some token").toFactory(someFactory);
135+
```
136+
137+
The `someFactory` function does not have to know that it creates an object for `some token`.
138+
139+
140+
### Default Bindings
141+
142+
Injector can create binding on the fly if we enable default bindings.
143+
144+
```
145+
var inj = new Injector([], {defaultBindings: true});
146+
var car = inj.get(Car); //this works as if `bind(Car).toClass(Car)` and `bind(Engine).toClass(Engine)` were present.
147+
```
148+
149+
This can be useful in tests, but highly discouraged in production.
150+
151+
152+
## Dependencies
153+
154+
A dependency can be synchronous, asynchronous, or lazy.
155+
156+
```
157+
class Car {
158+
constructor(@Inject(Engine) engine) {} // sync
159+
}
160+
161+
class Car {
162+
constructor(engine:Engine) {} // syntax sugar for `constructor(@Inject(Engine) engine:Engine)`
163+
}
164+
165+
class Car {
166+
constructor(@InjectPromise(Engine) engine:Promise) {} //async
167+
}
168+
169+
class Car {
170+
constructor(@InjectLazy(Engine) engineFactory:Function) {} //lazy
171+
}
172+
```
173+
174+
* The type annotation is used by DI only when no @Inject annotations are present.
175+
* `InjectPromise` tells DI to inject a promise (see the section on async for more information).
176+
* `InjectLazy` enables deferring the instantiation of a dependency by injecting a factory function.
177+
178+
179+
180+
## Async
181+
182+
Asynchronicity makes code hard to understand and unit test. DI provides two mechanisms to help with it: asynchronous bindings and asynchronous dependencies.
183+
184+
Suppose we have an object that requires some data from the server.
185+
186+
This is one way to implement it:
187+
188+
```
189+
class UserList {
190+
loadUsers() {
191+
this.usersLoaded = fetchUsersUsingHttp();
192+
this.usersLoaded.then((users) => this.users = users);
193+
}
194+
}
195+
196+
class UserController {
197+
constructor(ul:UserList){
198+
this.ul.usersLoaded.then((_) => someLogic(ul.users));
199+
}
200+
}
201+
```
202+
203+
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.
204+
205+
The DI library supports asynchronous bindings, which can be used to clean up UserList and UserController.
206+
207+
```
208+
class UserList {
209+
constructor(users:List){
210+
this.users = users;
211+
}
212+
}
213+
214+
class UserController {
215+
constructor(ul:UserList){
216+
}
217+
}
218+
219+
var inj = new Injector([
220+
bind(UserList).toAsyncFactory(() => fetchUsersUsingHttp().then((u) => new UserList(u))),
221+
UserController
222+
])
223+
224+
var uc:Promise = inj.asyncGet(UserController);
225+
```
226+
227+
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.
228+
229+
Note that asynchronicity have not disappeared. We just pushed out it of services.
230+
231+
DI also supports asynchronous dependencies, so we can make some of our services responsible for dealing with async.
232+
233+
```
234+
class UserList {
235+
constructor(users:List){
236+
this.users = users;
237+
}
238+
}
239+
240+
class UserController {
241+
constructor(@InjectPromise(UserList) ul:Promise){
242+
}
243+
}
244+
245+
var inj = new Injector([
246+
bind(UserList).toAsyncFactory(() => fetchUsersUsingHttp().then((u) => new UserList(u))),
247+
UserController
248+
])
249+
250+
var uc = inj.get(UserController);
251+
```
252+
253+
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.
254+
255+
256+
257+
### Cheat Sheet
258+
259+
#### Sync Binding + Sync Dependency:
260+
261+
```
262+
class UserList {
263+
}
264+
265+
class UserController {
266+
constructor(ul:UserList){}
267+
}
268+
269+
var inj = new Injector([UserList, UserController]);
270+
var ctrl:UserController = inj.get(UserController);
271+
```
272+
273+
#### Sync Binding + Async Dependency:
274+
275+
```
276+
class UserList {
277+
}
278+
279+
class UserController {
280+
constructor(@InjectPRomise(UserList) ul){}
281+
}
282+
283+
var inj = new Injector([UserList, UserController]);
284+
var ctrl:UserController = inj.get(UserController); //ctr.ul instanceof Promise;
285+
```
286+
287+
#### Async Binding + Sync Dependency:
288+
289+
```
290+
class UserList {
291+
}
292+
293+
class UserController {
294+
constructor(ul:UserList){}
295+
}
296+
297+
var inj = new Injector([
298+
bind(UserList).toAsyncFactory(() => fetchUsersUsingHttp().then((u) => new UserList(u))),
299+
UserController
300+
]);
301+
var ctrl:Promise = inj.asyncGet(UserController); //ctr.ul instanceof UserList;
302+
```
303+
304+
305+
#### Async Binding + Async Dependency:
306+
307+
```
308+
class UserList {
309+
}
310+
311+
class UserController {
312+
constructor(@InjectPromise(UserList) ul){}
313+
}
314+
315+
var inj = new Injector([
316+
bind(UserList).toAsyncFactory(() => fetchUsersUsingHttp().then((u) => new UserList(u))),
317+
UserController
318+
]);
319+
var ctrl = inj.get(UserController); //ctr.ul instanceof UserList;
320+
```
321+
322+
323+
324+
## Everything is Singleton
325+
326+
```
327+
inj.get(MyClass) === inj.get(MyClass); //always holds
328+
```
329+
330+
This holds even when we try to get the same token synchronously and asynchronously.
331+
332+
```
333+
var p = inj.asyncGet(MyClass);
334+
var mc = inj.get(MyClass);
335+
p.then((mc2) => mc2 === mc); // always holds
336+
```
337+
338+
### Transient Dependencies
339+
340+
If we need a transient dependency, something that we want a new instance of every single time, we have two options.
341+
342+
We can create a child injector:
343+
344+
```
345+
var child = inj.createChild([MyClass]);
346+
child.get(MyClass);
347+
```
348+
349+
Or we can register a factory function:
350+
351+
```
352+
var inj = new Injector([
353+
bind('MyClassFactory').toFactory(dep => () => new MyClass(dep), [SomeDependency]);
354+
]);
355+
356+
var inj.get('MyClassFactory')();
357+
```
358+
359+
360+
361+
## Key
362+
363+
Most of the time we do not have to deal with keys.
364+
365+
```
366+
var inj = new Injector([
367+
bind(Engine).toFactory(() => new TurboEngine()); //the passed in token Engine gets mapped to a key
368+
]);
369+
var engine = inj.get(Engine); //the passed in token Engine gets mapped to a key
370+
```
371+
372+
Now, the same example, but with keys
373+
374+
```
375+
var ENGINE_KEY = Key.get(Engine);
376+
377+
var inj = new Injector([
378+
bind(ENGINE_KEY).toFactory(() => new TurboEngine()); // no mapping
379+
]);
380+
var engine = inj.get(ENGINE_KEY); // no mapping
381+
```
382+
383+
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.

0 commit comments

Comments
 (0)