Skip to content

Commit f876a9e

Browse files
committed
footnoteref:
1 parent c8f1b1e commit f876a9e

File tree

5 files changed

+10
-10
lines changed

5 files changed

+10
-10
lines changed

chapters/ch02.asciidoc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,15 +330,15 @@ export default factory
330330

331331
As long as we limit the usage of each counter spewed by the `factory` to a given portion of the application which knows about each other usage, the state becomes more manageable, as we end up with fewer moving parts involved. When we eliminate impurity in public interfaces, we're effectively circumscribing entropy to the calling code. The consumer receives a brand new counter every time, and it's entirely responsible for managing its state. It can still pass the `counter` down to its dependents, but it's in control of how dependents get to manipulate that state, if at all.
332332

333-
This is something we observe in the wild, with popular libraries such as the `request` packagefootnote:[You can find `request` here: https://mjavascript.com/out/request.] in Node.js, which can be used to make HTTP requests. The `request` function relies largely on sensible defaults for the `options` you can pass to it. Sometimes, we want to make requests using a different set of defaults.
333+
This is something we observe in the wild, with popular libraries such as the `request` packagefootnoteref:[request-pkg,You can find `request` here: https://mjavascript.com/out/request.] in Node.js, which can be used to make HTTP requests. The `request` function relies largely on sensible defaults for the `options` you can pass to it. Sometimes, we want to make requests using a different set of defaults.
334334

335335
The library might've offered a solution where we could change the default values for every call to `request`. This would've been poor design, as it'd make their handling of `options` more unstable, where we'd have to take into account every corner of our codebase before we could be confident about the `options` we'd ultimately end up with when calling `request`.
336336

337337
Request chose a solution where it has a `request.defaults(options)` method which returns an API identical to that of `request`, but with the new defaults applied on top of the existing defaults. This way it avoids surprises, since usage of the modified `request` is constrained to the calling code and its dependents.
338338

339339
=== 2.2 CRUST: Consistent, Resilient, Unambiguous, Simple and Tiny
340340

341-
A well-regarded API typically packs several of the following traits. It is consistent, meaning it is idempotentfootnote:[For a given set of inputs, an idempotent function always produces the same output.] and has a similar signature shape as that of related functions. It is resilient, meaning its interface is flexible and accepts input expressed in a few different ways, including optional parameters and overloading. Yet, it is unambiguous, there aren't multiple interpretations of how the API should be used, what it does, how to provide inputs or how to understand the output. Through all of this, it manages to stay simple: it's straightforward to use and it handles common use cases with little to no configuration, while allowing customization for advanced use cases. Lastly, a CRUST interface is also tiny: it meets its goals but it isn't overdesigned, it's comprised by the smallest possible surface area while allowing for future non-breaking extensibility. CRUST mostly regards the outer layer of a system (be it a package, a file, or a function), but its principles will seep into the innards of its components and result in simpler code overall.
341+
A well-regarded API typically packs several of the following traits. It is consistent, meaning it is idempotentfootnoteref:[idempotence-def,For a given set of inputs, an idempotent function always produces the same output.] and has a similar signature shape as that of related functions. It is resilient, meaning its interface is flexible and accepts input expressed in a few different ways, including optional parameters and overloading. Yet, it is unambiguous, there aren't multiple interpretations of how the API should be used, what it does, how to provide inputs or how to understand the output. Through all of this, it manages to stay simple: it's straightforward to use and it handles common use cases with little to no configuration, while allowing customization for advanced use cases. Lastly, a CRUST interface is also tiny: it meets its goals but it isn't overdesigned, it's comprised by the smallest possible surface area while allowing for future non-breaking extensibility. CRUST mostly regards the outer layer of a system (be it a package, a file, or a function), but its principles will seep into the innards of its components and result in simpler code overall.
342342

343343
That's a lot to take in. Let's try and break down the CRUST principle. In this section we explore each trait, detailing what they mean and why it's important that our interfaces follow each of them.
344344

@@ -404,7 +404,7 @@ await fetch('/api/users/rob', {
404404
})
405405
----
406406

407-
Supposing that -- if we were the API designers for `fetch` -- we originally devised `fetch` as just a way of doing `GET ${ resource }`. When we got a requirement for a way of choosing the HTTP method, we could've avoided the options object and reached directly for a `fetch(resource, method)` overload. While this would've served our particular requirement, it would've been short-sighted. As soon as we got a requirement to configure something else, we'd be left with the need of supporting both `fetch(resource, method)` and `fetch(resource, options)` overloads, so that we avoid breaking backward compatibility. Worse still, we might be tempted to introduce a third parameter that configures our next requirement. Soon, we'd end up with an API such as the infamous `KeyboardEvent#initKeyEvent` methodfootnote:[See the MDN documentation at https://mjavascript.com/out/initkeyevent.], whose signature is outlined below.
407+
Supposing that -- if we were the API designers for `fetch` -- we originally devised `fetch` as just a way of doing `GET ${ resource }`. When we got a requirement for a way of choosing the HTTP method, we could've avoided the options object and reached directly for a `fetch(resource, method)` overload. While this would've served our particular requirement, it would've been short-sighted. As soon as we got a requirement to configure something else, we'd be left with the need of supporting both `fetch(resource, method)` and `fetch(resource, options)` overloads, so that we avoid breaking backward compatibility. Worse still, we might be tempted to introduce a third parameter that configures our next requirement. Soon, we'd end up with an API such as the infamous `KeyboardEvent#initKeyEvent` methodfootnoteref:[mdn-initkeyevent,See the MDN documentation at https://mjavascript.com/out/initkeyevent.], whose signature is outlined below.
408408

409409
[source,javascript]
410410
----
@@ -425,7 +425,7 @@ event.initKeyEvent(type, { bubbles, cancelable, viewArg,
425425
A key aspect of API design is readability. How far can users get without having to reach for the documentation? In the case of `initKeyEvent`, not very, unless they memorize the position of each of 10 parameters, and their default values, chances are they're going to reach for the documentation every time. When designing an interface that might otherwise end up with four or more parameters, an `options` object carries a multitude of benefits:
426426

427427
- The consumer can declare options in any order, as the arguments are no longer positional inside the `options` object
428-
- The API can offer default values for each option. This helps the consumer avoid specifying defaults just so that they can change another positional parameterfootnote:[Assuming we have a `createButton(size = 'normal', type = 'primary', color = 'red')` method and we want to change its color, we'd have to do `createButton('normal', 'primary', 'blue')` to accomplish that, only because the API didn't have an `options` object. If the API ever changes its defaults, we'd have to change any function calls accordingly as well.]
428+
- The API can offer default values for each option. This helps the consumer avoid specifying defaults just so that they can change another positional parameterfootnoteref:[api-readability,Assuming we have a `createButton(size = 'normal', type = 'primary', color = 'red')` method and we want to change its color, we'd have to do `createButton('normal', 'primary', 'blue')` to accomplish that, only because the API didn't have an `options` object. If the API ever changes its defaults, we'd have to change any function calls accordingly as well.]
429429
- The consumer doesn't need to concern herself with options they don't need
430430
- Developers reading pieces of code which consume the API can immediately understand what parameters are being used, since they're explicitly named in the options object
431431

chapters/ch03.asciidoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Taken literally, moving fast and breaking things is a dreadful way to go about s
8181

8282
The code that makes up a product should be covered by tests, minimizing the risk of bugs making their way to production. When we take "Move Fast and Break Things" literally, we are tempted to think testing is optional, since it slows us down and we need to move fast. A product that's not test covered will be, ironically, unable to move fast when bugs inevitable arise and wind down engineering speed.
8383

84-
A better mantra might be one that can be taken literally, such as "Move Deliberately and Experiment". This mantra carries the same sentiment as the Facebook mantra of "Move Fast and Break Things", but its true meaning isn't meant to be decoded or interpreted. Experimentation is a key aspect of software design and development. We should constantly try out and validate new ideas, verifying whether they pose better solutions than the status quo. We could interpret "Move Fast and Break Things" as "A/B testfootnote:[A/B testing is a form of user testing where we take a small portion of users and present them with a different experience than what we present to the general userbase. We then track engagement among the two groups, and if the engagement is higher for the users with the new experience, then we might go ahead and present that to our entire userbase. It is an effective way of reducing risk when we want to modify our user experience, by testing our assumptions in small experiments before we introduce changes to the majority of our users.] early and A/B test often", and "Move Deliberately and Experiment" can convey this meaning as well.
84+
A better mantra might be one that can be taken literally, such as "Move Deliberately and Experiment". This mantra carries the same sentiment as the Facebook mantra of "Move Fast and Break Things", but its true meaning isn't meant to be decoded or interpreted. Experimentation is a key aspect of software design and development. We should constantly try out and validate new ideas, verifying whether they pose better solutions than the status quo. We could interpret "Move Fast and Break Things" as "A/B testfootnoteref:[ab-testing,A/B testing is a form of user testing where we take a small portion of users and present them with a different experience than what we present to the general userbase. We then track engagement among the two groups, and if the engagement is higher for the users with the new experience, then we might go ahead and present that to our entire userbase. It is an effective way of reducing risk when we want to modify our user experience, by testing our assumptions in small experiments before we introduce changes to the majority of our users.] early and A/B test often", and "Move Deliberately and Experiment" can convey this meaning as well.
8585

8686
To move deliberately is to move with cause. Engineering tempo will rarely be guided by the development team's desire to move faster, but is most often instead bound by release cycles and the complexity in requirements needed to meet those releases. Of course, everyone wants engineering to move fast where possible, but interface design shouldn't be hurried, regardless of whether the interface we're dealing with is an architecture, a layer, a component, or a function. Internals aren't as crucial to get right, for as long as the interface holds, the internals can be later improved for performance or readability gains. This is not to advocate sloppily developed internals, but rather to encourage respectfully and deliberately thought out interface design.
8787

@@ -187,7 +187,7 @@ Tom Preston-Werner wrote about the notion of README-driven development as a way
187187

188188
There's a popular phrase in the world of CSS about how it's an "append-only language" implicating that once a piece of CSS code has been added it can't be removed any longer, because doing so could inadvertently break our designs, due to the way the cascade works. JavaScript doesn't make it quite that hard to remove code, but it's indeed a highly dynamic language, and removing code with the certainty that nothing will break remains a bit of a challenge as well.
189189

190-
Naturally, it's easier to modify a module's internal implementation than to change its public API, as the effects of doing so would be limited to the module's internals. Internal changes that don't affect the API are typically not observable from the outside. The exception to that rule would be when consumers monkey-patchfootnote:[Monkey-patching is when we intentionally modify the public interface of a component from the outside in order to add, remove, or modify its functionality. Monkey-patching can be helpful when we want to change the behavior of a component we don't control, such as a library or dependency. Patching is error-prone because we might be affecting other consumers of this API, who are unaware of our patches. The API itself or its internals may also change, breaking the assumptions made about them in our patch. While it's generally best avoided, sometimes it's the only choice at hand.] our interface, sometimes becoming able to observe some of our internals. In this case, however, the consumer should be aware of how brittle monkey-patching a module they do not control is, and they did so assuming the risk of breakage.
190+
Naturally, it's easier to modify a module's internal implementation than to change its public API, as the effects of doing so would be limited to the module's internals. Internal changes that don't affect the API are typically not observable from the outside. The exception to that rule would be when consumers monkey-patchfootnoteref:[monkey-patching,Monkey-patching is when we intentionally modify the public interface of a component from the outside in order to add, remove, or modify its functionality. Monkey-patching can be helpful when we want to change the behavior of a component we don't control, such as a library or dependency. Patching is error-prone because we might be affecting other consumers of this API, who are unaware of our patches. The API itself or its internals may also change, breaking the assumptions made about them in our patch. While it's generally best avoided, sometimes it's the only choice at hand.] our interface, sometimes becoming able to observe some of our internals. In this case, however, the consumer should be aware of how brittle monkey-patching a module they do not control is, and they did so assuming the risk of breakage.
191191

192192
In section 3.1.2 we observed that the best code is no code at all, and this has implications when it comes to removing code as well. Code we never write is code we don't need to worry about deleting. The less code there is, the less code we need to maintain, the less potential bugs we are yet to uncover, and the less code we need to read, test, and deliver over mobile networks to speed-hungry humans.
193193

chapters/ch04.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ if (hasValidToken(auth)) {
104104
}
105105
----
106106

107-
We could've used more variables without creating a function, inlining the computation of `hasValidToken` right before the `if` check. A crucial difference between the function-based refactor and the inlining solution is that we used a short-circuiting `return` statement to preemptively bail when we already knew the token was invalidfootnote:[In the example, we immediately return `false` when the token isn't present.], however we can't use `return` statements to bail from the snippet of code that computes `hasValidToken` in the following piece of code without coupling its computation to knowledge about what the routine should return for failure cases. As a result, our only options are tightly coupling the inline subroutine to its containing function, or using a logical or ternary operator in the intermediate steps of the inlined computation.
107+
We could've used more variables without creating a function, inlining the computation of `hasValidToken` right before the `if` check. A crucial difference between the function-based refactor and the inlining solution is that we used a short-circuiting `return` statement to preemptively bail when we already knew the token was invalidfootnoteref:[return-early,In the example, we immediately return `false` when the token isn't present.], however we can't use `return` statements to bail from the snippet of code that computes `hasValidToken` in the following piece of code without coupling its computation to knowledge about what the routine should return for failure cases. As a result, our only options are tightly coupling the inline subroutine to its containing function, or using a logical or ternary operator in the intermediate steps of the inlined computation.
108108

109109
[source,javascript]
110110
----

0 commit comments

Comments
 (0)