You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: chapters/ch04.asciidoc
+93-3Lines changed: 93 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -131,7 +131,7 @@ When we have a long branch inside a conditional statement, chances are we're doi
131
131
----
132
132
if (response) {
133
133
if (!response.errors) {
134
-
// ... use `response`
134
+
// … use `response`
135
135
} else {
136
136
return false
137
137
}
@@ -152,7 +152,7 @@ if (!response) {
152
152
if (response.errors) {
153
153
return false
154
154
}
155
-
// ... use `response`
155
+
// … use `response`
156
156
----
157
157
158
158
This early exit approach is often referred to as _guard clauses_, and one of their biggest benefits is that we can learn all the failure cases upon reading the first few lines of a function or piece of code. We're not limited to `return` statements: we could `throw` errors in a promise-based context or in an async function, and in callback chaining contexts we might opt for a `done(error)` callback followed by a `return` statement.
@@ -209,7 +209,97 @@ Given the stack-based nature of programming, it's not that easy to naturally app
209
209
210
210
==== 4.2.3 Extracting Functions
211
211
212
-
e.g let with no assignment to const, moving mappers to bottom, anything that gets in the way of *current* flow we defer for later.
212
+
Deliberate, pyramidal structures where we deal with higher level concerns near the top and switch to more specific problems as we go deeper into the inner workings of a system works wonders in keeping complexity on a tight leash. Such structures are particularly powerful because they break up complex items into their own individual units near the flat bottom of the system, avoiding a complicated interweaving of concerns that are fuzzied together, becoming undistinguishable from one another over time.
213
+
214
+
Pushing anything that gets in the way of the current flow to the bottom of a function is an effective way of streamlining readability. As an example, consider the case where we have a non-trivial mapper inline in the heart of a function. In the following code snippet we're mapping the users into user models, as we often need to do when we prepare JSON responses for API calls.
215
+
216
+
[source,javascript]
217
+
----
218
+
function getUserModels(done) {
219
+
findUsers((err, users) => {
220
+
if (err) {
221
+
done(err)
222
+
return
223
+
}
224
+
225
+
const models = users.map(user => {
226
+
const { name, email } = user
227
+
const model = { name, email }
228
+
if (user.type.includes('admin')) {
229
+
model.admin = true
230
+
}
231
+
return model
232
+
})
233
+
234
+
done(null, models)
235
+
})
236
+
}
237
+
----
238
+
239
+
Now compare that code to the following bit of code, where we extracted the mapping function and shoved it out of the way. Given the mapping function doesn't need any of the scope from `getUserModels`, we can pull it out of that scope entirely, without the need to place `toUserModel` at the bottom of the `getUserModels` function. This means we can now also reuse `toUserModel` in other routines, we don't have to wonder whether the function actually depends on any of the contaning scope's context anymore, and `getUserModels` is now focused on the higher level flow where we find users, map them to their models, and return them.
240
+
241
+
[source,javascript]
242
+
----
243
+
function getUserModels(done) {
244
+
findUsers((err, users) => {
245
+
if (err) {
246
+
done(err)
247
+
return
248
+
}
249
+
250
+
const models = users.map(toUserModel)
251
+
252
+
done(null, models)
253
+
})
254
+
}
255
+
256
+
function toUserModel(user) {
257
+
const { name, email } = user
258
+
const model = { name, email }
259
+
if (user.type.includes('admin')) {
260
+
model.admin = true
261
+
}
262
+
return model
263
+
}
264
+
----
265
+
266
+
Furthermore, if there were additional work to be done between the mapping and the callback, it could also be moved into another small function that wouldn't get in the way of our higher level `getUserModels` function.
267
+
268
+
A similar case occurs when we have a variable that gets defined based on a condition, as shown in the next snippet. Bits of code like this can distract the reader away from the core purpose of a function, to the point where they're often ignored or glossed over.
269
+
270
+
[source,javascript]
271
+
----
272
+
// …
273
+
let website = null
274
+
if (user.details) {
275
+
website = user.details.website
276
+
} else if (user.website) {
277
+
website = user.website
278
+
}
279
+
// …
280
+
----
281
+
282
+
It's best to refactor this kind of assignments into a function, like the one shown next. Note how we've included a `user` parameter so that we can push the function out of the scope chain where we've originally defined the user object, and at the same time went from a `let` binding to a `const` binding. When reading this piece of code later down the line, the benefit of `const` is that we'll know the binding won't change, as opposed to `let` with which we can't be certain bindings won't change over time, adding to the pile of things the reader should be watching out for when trying to understand the algorithm.
0 commit comments