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