1 | # depject
|
2 |
|
3 | > simplest dependency injection
|
4 |
|
5 | ## Installation
|
6 |
|
7 | ```sh
|
8 | $ npm install --save depject
|
9 | ```
|
10 |
|
11 | ## philosophy
|
12 |
|
13 | A module exposes features to be used by other modules,
|
14 | and may also depend on features provided by other modules.
|
15 | Any module system can do that. In the node module system,
|
16 | modules declare exactly which modules they depend on.
|
17 | That works well when the module does a very well defined task,
|
18 | that can be abstractly solved. In other words, it works well
|
19 | when the module solves a technical problem.
|
20 |
|
21 | But it doesn't work so well when the module just represents an opinion.
|
22 | Developer tools seem to be dominated by technical problems,
|
23 | but user applications seem to be dominated by opinions.
|
24 | There are many different ways something could be implemented,
|
25 | no objectively optimal solution, and loads of pretty good ones.
|
26 |
|
27 | The contemporary best practice is to embrace that, and create software
|
28 | that has strong opinions. That takes a strong leader to make decisions,
|
29 | compromises be dammed. I am building a p2p system, and have gone to
|
30 | considerable effort to create a decentralized protocol. But then,
|
31 | if we have a user interface with strong opinions, then that recentralizes development.
|
32 |
|
33 | My strong opinion is to reject strong opinions. `depject` is a strategy to
|
34 | deopinionate software. It should be easy to change any particular opinion.
|
35 |
|
36 | Another way to look at this, is the goal is to make pull-requests that merge easily.
|
37 | with node's module system, a dependant module must declare exactly which modules they depend on.
|
38 | That means, to add a feature, you need to add a new file implementing it,
|
39 | and also update files that use that.
|
40 |
|
41 | To contrast, in `depject` if that feature is the same _shape_ as one already existing,
|
42 | you only need to add that file. This means you can add merge two new features,
|
43 | with out a conflict.
|
44 |
|
45 | ## patterns
|
46 |
|
47 | ### first - use the first module that has an opinion about a thing.
|
48 |
|
49 | Say we have a system with multiple types of messages. Each type has a renderer.
|
50 | We want to call all the renderers, and get the first one that knows how to handle that value.
|
51 |
|
52 | ### map - get each module's opinion about a thing.
|
53 |
|
54 | Say we have a menu that is actions which may be performed on a thing.
|
55 | We map the modules over that thing, and add all returned items to a menu.
|
56 |
|
57 | ### reduce - compose each modules opinion about a thing into one opinion.
|
58 |
|
59 | We might want to allow other modules to decorate the value given by our module
|
60 |
|
61 | ## example
|
62 |
|
63 | ### Using `first`
|
64 |
|
65 | ```js
|
66 | const combine = require('depject')
|
67 |
|
68 | const cats = {
|
69 | gives: 'animalSound',
|
70 | create: () => (type) => {
|
71 | if(type !== 'cat') return
|
72 | return 'Meow'
|
73 | }
|
74 | }
|
75 |
|
76 | const dogs = {
|
77 | gives: 'animalSound',
|
78 | create: () => (type) => {
|
79 | if(type !== 'dog') return
|
80 | return 'Woof'
|
81 | }
|
82 | }
|
83 |
|
84 | const speak = {
|
85 | needs: {animalSound: 'first'},
|
86 | gives: 'speak',
|
87 | create: (api) => api.animalSound
|
88 | }
|
89 |
|
90 | const sockets = combine([cats, dogs, speak])
|
91 |
|
92 | const mySpeak = sockets.speak[0]
|
93 |
|
94 | console.log(mySpeak('dog'))
|
95 | //Woof
|
96 | ```
|
97 |
|
98 | ### Using `map`
|
99 |
|
100 | ```js
|
101 | const combine = require('depject')
|
102 |
|
103 | const cats = {
|
104 | gives: 'name',
|
105 | create: () => () => 'Fluffy'
|
106 | }
|
107 |
|
108 | const dogs = {
|
109 | gives: 'name',
|
110 | create: () => () => 'Rex'
|
111 | }
|
112 |
|
113 | const animals = {
|
114 | needs: {name: 'map'},
|
115 | gives: 'animals',
|
116 | create: (api) => api.name
|
117 | }
|
118 |
|
119 | var sockets = combine([cats, dogs, animals])
|
120 |
|
121 | var myAnimals = sockets.animals[0]
|
122 |
|
123 | console.log(myAnimals())
|
124 | //['Fluffy', 'Rex']
|
125 | ```
|
126 |
|
127 | ## api
|
128 |
|
129 | ### modules
|
130 |
|
131 | Each module is an object which exposes `{needs, gives, create}` properties. `needs` and `gives` describe the module features that this module requires, and exports.
|
132 |
|
133 | `needs` is a map of names to types. `{<name> : "map"|"first"|"reduce"}`
|
134 |
|
135 | `gives` Is a string name of it's export, or if there are multiple exports an object where each key is a name `{<name>: true,...}`.
|
136 |
|
137 | `create` Is a function that is called with an object connected to modules which provide the `needs` and must return a value which provides the `gives` or an object with keys that match what the module `gives`.
|
138 |
|
139 | ### combine
|
140 |
|
141 | Actually connect all the modules together!
|
142 | Takes an array of modules, resolves dependencies and injects them into each module.
|
143 |
|
144 | `combine([modules...])`
|
145 |
|
146 | This will return an array object of arrays of exports.
|
147 |
|
148 | ## exporting more than one thing from a module
|
149 |
|
150 | ```js
|
151 | const cats = {
|
152 | gives: {name: true, animalSound: true},
|
153 | create: () => ({
|
154 | name: () => 'Fluffy',
|
155 | animalSound: () => {
|
156 | if(type !== 'cat') return
|
157 | return 'Meow'
|
158 | }
|
159 | })
|
160 | }
|
161 | ```
|
162 |
|
163 | ## requiring more than one thing into a module
|
164 |
|
165 | ```js
|
166 | const animalSounds = {
|
167 | needs: {name: 'map', animalSound: 'first'}
|
168 | }
|
169 | ```
|
170 |
|
171 | ## deeply nested modules
|
172 |
|
173 | It's possible to pass deeply nested modules to combine eg:
|
174 |
|
175 | ```js
|
176 | const modules = {
|
177 | a: {
|
178 | b: {
|
179 | c: {
|
180 | gives: 'yes',
|
181 | create: function () {
|
182 | return function () {
|
183 | return true
|
184 | }
|
185 | }
|
186 | }
|
187 | },
|
188 | d: {
|
189 | e: {
|
190 | needs: {
|
191 | yes: 'first'
|
192 | },
|
193 | gives: 'no',
|
194 | create: function (api) {
|
195 | return function () {
|
196 | return !api.yes()
|
197 | }
|
198 | }
|
199 | }
|
200 | }
|
201 | }
|
202 | }
|
203 |
|
204 | const api = combine(modules)
|
205 | ```
|
206 |
|
207 | ### design questions
|
208 |
|
209 | Should `combine` have a way to specify the public interface?
|
210 |
|
211 | Should there be a way to create a routed plugin?
|
212 | i.e. check a field and call a specific plugin directly?
|
213 |
|
214 | How does this interact with interfaces provided remotely?
|
215 | i.e. muxrpc?
|
216 |
|
217 | ## License
|
218 |
|
219 | MIT © [Dominic Tarr](http://dominictarr.com)
|