UNPKG

11.9 kBMarkdownView Raw
1# glimmer-di [![Build Status](https://secure.travis-ci.org/glimmerjs/glimmer-di.svg?branch=master)](http://travis-ci.org/glimmerjs/glimmer-di)
2
3Dependency injection for Glimmer applications.
4
5## What is Dependency Injection?
6
7Dependency injection is a pattern that increases the flexibility, testability
8and consistency of your code.
9
10The three key ideas are:
11
121. An object's dependencies (that is, the other objects it needs to do its job)
13 should be provided to the object when it is created, rather than hard-coded.
142. A dependency may have multiple implementations, so long as each
15 implementation adheres to an agreed-upon interface.
163. An object using a dependency shouldn't care where on the filesystem that
17 dependency comes from.
18
19Let's look at a short example that **does not** use dependency injection. We'll
20write a hypothetical server that renders a short HTML document when an incoming
21request is received:
22
23```js
24import HTTPServer from "./servers/http";
25
26export default class HelloWorldServer {
27 constructor() {
28 let server = new HTTPServer({
29 port: 80
30 });
31
32 server.on('request', req => {
33 req.write("<html><body>Hello, world!</body></html>");
34 });
35 }
36}
37```
38
39This is great, but there's one problem. As you can see, our Hello World server
40is importing the HTTP library directly. If we want to support both HTTP and
41HTTP/2 (or even something like a WebSocket), this code is not reusable.
42
43We would have to either duplicate this code, or add some configuration options
44to let the user tell us which protocol they want to use. Of course, that would
45work today, but if we wanted to support HTTP/3 in the future, we'd have to come
46back and add a new configuration option for every new protocol.
47
48What if, instead of telling the server what protocol to use, we could instead
49provide it with an object that encapsulated all of those concerns?
50
51Instead of having our `HelloWorldServer` import and instantiate `HTTPServer`
52directly, we can provide it with an object that we guarantee implements the same
53interface. In this case, that means any object that emits a `'request'` event
54and supports adding an event listener with the `on()` method.
55
56Let's look at what that updated example might look like:
57
58```js
59export default class HelloWorldServer {
60 constructor(server) {
61 server.on('request', req => {
62 req.write("<html><body>Hello, world!</body></html>");
63 });
64 }
65}
66```
67
68Now we're no longer concerned with instantiating and configuring an HTTP server.
69All we have to know is that whatever object gets passed to our class has an
70`on()` method that lets us add an event listener.
71
72Now, let's look at a few different ways we can use our newly improved Hello
73World server.
74
75```js
76import HelloWorldServer from "./hello-world-server";
77import HTTPServer from "./servers/http";
78import HTTP2Server from "./servers/http2";
79import WebSocketServer from "./servers/web-socket";
80
81// HTTP 1
82let httpServer = new HTTPServer({
83 port: 80
84});
85new HelloWorldServer(httpServer);
86
87// HTTP 2
88let http2Server = new HTTP2Server({
89 port: 4200
90});
91new HelloWorldServer(http2Server);
92
93// WebSocket
94let wsServer = new WebSocketServer();
95new HelloWorldServer(wsServer);
96```
97
98With that one small change, we've dramatically improved the reusability and
99flexibility of our Hello World server. It can now handle any protocol, even ones
100that didn't exist when it was written, so long as they can be adapted to follow
101the simple interface we've defined.
102
103This idea may seem simple, but it has profound implications for managing the
104complexity of your code as your application grows. And it means that you can
105swap in different pieces of code easily depending on the environment.
106
107For example, in unit tests we may want to swap in some stub objects to verify
108some behavior. Dependency injection makes it easy and avoids having to override
109global values.
110
111We can also make it possible to run the same application on both Node.js and the
112browser, by swapping in one piece of framework code when you have a full DOM
113implementation and another implementation when you don't.
114
115While dependency injection is just a simple pattern, it helps to have that
116pattern formalized into code. That's exactly what this library does: implement
117an incredibly lightweight version of dependency injection, with some utilities
118to help us clean up after ourselves when we're done running the app.
119
120## Containers and Registries
121
122The two core parts of the Glimmer DI system are the `Registry` and the `Container`.
123
124Here's how to remember the role of each:
125
1261. The `Registry` is where you **register** code (that is, JavaScript classes).
1272. The `Container` **contains** objects, and is where you request instances of
128 registered classes.
129
130If that sounds confusing, let's look at an example that should make it
131clearer.
132
133Let's say I have a class for a UI component that I want to make available to the
134system. The first thing I would do is create a new `Registry` instance and tell
135it about my class.
136
137```js
138import { Registry } from '@glimmer/di';
139import ProfileComponent from './components/profile';
140
141let registry = new Registry();
142registry.register('component:profile', ProfileComponent);
143```
144
145You probably noticed the string that we're passing to the `register` method:
146`'component:profile'`. This is what we call a _specifier_, which is a unique
147identifier for a class. They take the form of `${type}:${name}`. In this case,
148we have a UI component called `Profile` so its specifier would be
149`'component:profile'`. If instead we had an blog post model, its specifier might
150be `'model:blog-post'`.
151
152So now we've told the `Registry` about our component. Let's get an instance of
153that component now. To do that, we'll need to create a new `Container`, tell it
154about our registry, and then ask it for the component we want:
155
156```js
157import { Container } from '@glimmer/di';
158
159// Create the container and pass in the registry we previously created.
160let container = new Container(registry);
161let component = container.lookup('component:profile');
162```
163
164Now our `component` variable contains an instance of the previously-registered
165profile component.
166
167### Singletons
168
169One important thing to note is that (by default) every time you call the
170`lookup` method, you'll get the same instance of the component:
171
172```js
173let component1 = container.lookup('component:profile');
174let component2 = container.lookup('component:profile');
175
176component1 === component2; // => true
177```
178
179But that's not the behavior we want: in an app, you need to be able to create
180many instances of the same component.
181
182In this case, we want to change the default behavior and tell the registry that
183we should always get a _new_ instance when we call
184`lookup('component:profile')`:
185
186```js
187registry.registerOption('component:profile', 'singleton', false);
188```
189
190Here, we've set the `singleton` option to `false` for this component. We could
191have also configured this setting back when we originally registered the component:
192
193```js
194registry.register('component:profile', ProfileComponent, {
195 singleton: false
196});
197```
198
199Now if we lookup multiple components, we'll get a different instance each time:
200
201```js
202let component3 = container.lookup('component:profile');
203let component4 = container.lookup('component:profile');
204
205component3 === component4; // => false
206```
207
208### Injections
209
210So far, this doesn't seem to offer any benefits over just instantiating the
211class ourselves whenever we need a new instance. Let's look at one of the killer
212features: injections.
213
214An _injection_ is a rule that tells the container to automatically give one object
215access to another.
216
217For example, let's imagine we have a centralized data store that we want to make
218available to all of our components, so they can retrieve model data over the
219network. Without worrying about how components get created in our framework, we
220just want to say: "every time a new component is instantiated, make sure it has
221access to the data store."
222
223We can set this up automatically with an injection. First, let's register the
224data store with the registry:
225
226```js
227import DataStore from "./data/store";
228
229registry.register('store:main', DataStore);
230```
231
232Because we want components to share a single store instance, note that we didn't
233disable the default `singleton` setting. For the whole app, there will be just
234one store.
235
236(If there's only one instance of a particular type in an app, we often call it
237`main`. In this case, because there's one store and it's a singleton, its
238specifier is `store:main`. There's nothing special about this name, though; it's
239just a common convention.)
240
241Next, we'll create a rule that tells the registry that new components should be
242provided with the data store instance:
243
244```js
245registry.registerInjection('component', 'store', 'store:main');
246```
247
248Let's look at each of these arguments to `registerInjection`. Each one helps define part
249of the injection rule. In this case, it means:
250
2511. For every new `component` created,
2522. Set its `store` property to
2533. The instance of `store:main`
254
255In other words, every time `container.lookup('component:profile')` gets called,
256something like this is happening under the hood:
257
258```js
259let store = container.lookup('store:main');
260return ProfileComponent.create({ store });
261```
262
263The nice thing about injections is that we can set up a rule once and not worry
264about the details of where and how instances actually get created. This
265separation of concerns allows for less brittle code.
266
267You've also now seen why specifiers contain information about both name and
268type. Injections let us specify rules that apply to all instances of a
269component, say, without having to repeat that rule for every component in the
270system.
271
272## Resolvers: Mapping to the File System
273
274So far, we've always had to tell the `Registry` about a class before we're able
275to get an instance from the `Container`. But if we're being good developers, and
276organizing our code well and being consistent in our naming, shouldn't our app
277be able to find our classes automatically?
278
279That's exactly what the `Resolver` helps us do. With a resolver, we can define
280rules that map specifiers (like `component:profile`) on to module names (like
281`app/components/profile.js`).
282
283A simple `Resolver` implements a single method, `retrieve()`, which takes a
284specifier and returns the associated class.
285
286Lets write a resolver that will load the component class using CommonJS instead of
287having to eagerly register every component in our app:
288
289```js
290class Resolver {
291 retrieve(specifier) {
292 let [type, name] = specifier.split(':');
293
294 if (type !== 'component') { throw new Error("Unsupported type"); }
295 return require(`./app/${type}s/${name}.js`);
296 }
297}
298
299let registry = new Registry();
300let resolver = new Resolver();
301let container2 = new Container(registry, resolver);
302
303// Make sure components aren't singletons
304registry.registerOption('component', 'singleton', false);
305
306// Requires and instantiates `./app/components/admin-page.js`:
307let adminPage = container2.lookup('component:admin-page');
308```
309
310Note that `retrieve()` *must* return synchronously. Your module loader therefore
311must return synchronously, as it does in this CommonJS example. If you're using an
312asynchronous module loader, you'll need to make sure modules are loaded before you
313start instantiating objects.
314
315As a general rule, this package is designed to be synchronous to achieve maximum
316performance; it is your responsibility to ensure that code is ready before it is
317needed.
318
319One last thing: you may have noticed that the container in this example has both
320a registry and a resolver. The container will look for classes in both, but the
321registry always takes precedence. If the registry is empty, the container will
322fall back to asking the resolver for its help.
323
324## Acknowledgements
325
326Thanks to [Monegraph](http://monegraph.com) and
327[Cerebris](http://www.cerebris.com) for funding the initial development of this
328library.
329
330## License
331
332MIT License.