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/ch02.asciidoc
+4-4Lines changed: 4 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -330,15 +330,15 @@ export default factory
330
330
331
331
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.
332
332
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.
334
334
335
335
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`.
336
336
337
337
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.
338
338
339
339
=== 2.2 CRUST: Consistent, Resilient, Unambiguous, Simple and Tiny
340
340
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.
342
342
343
343
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.
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.
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:
426
426
427
427
- 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.]
429
429
- The consumer doesn't need to concern herself with options they don't need
430
430
- 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
Copy file name to clipboardExpand all lines: chapters/ch03.asciidoc
+2-2Lines changed: 2 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -81,7 +81,7 @@ Taken literally, moving fast and breaking things is a dreadful way to go about s
81
81
82
82
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.
83
83
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.
85
85
86
86
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.
87
87
@@ -187,7 +187,7 @@ Tom Preston-Werner wrote about the notion of README-driven development as a way
187
187
188
188
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.
189
189
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.
191
191
192
192
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.
Copy file name to clipboardExpand all lines: chapters/ch04.asciidoc
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -104,7 +104,7 @@ if (hasValidToken(auth)) {
104
104
}
105
105
----
106
106
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.
0 commit comments