From d2b22276f3e9fcfdb1887f9f224e09222bad5b28 Mon Sep 17 00:00:00 2001 From: Tiago Hermano Date: 2018年9月30日 19:44:51 -0300 Subject: [PATCH 1/9] fixed grammar typos --- chapters/ch06.asciidoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chapters/ch06.asciidoc b/chapters/ch06.asciidoc index c15cf42..f0239ef 100644 --- a/chapters/ch06.asciidoc +++ b/chapters/ch06.asciidoc @@ -173,7 +173,7 @@ Every dependency in our application should be explicitly declared in our manifes Using the information in a package lock file, which contains details about every package we depend upon and all of their dependencies as well, package managers can take steps to install the same bits every time, preserving our ability to quickly iterate and install package updates, while keeping our code safe. -Always installing identical versions of our dependencies -- and identical versions of our dependencies' dependencies -- brings us one step closer to having development environments that closely mirror what we do in production. This increases the likelyhood we can swiftly reproduce bugs that occurred in production in our local environments, while decreasing the odds that something that worked during development fails in staging. +Always installing identical versions of our dependencies -- and identical versions of our dependencies' dependencies -- brings us one step closer to having development environments that closely mirror what we do in production. This increases the likelihood we can swiftly reproduce bugs that occurred in production in our local environments, while decreasing the odds that something that worked during development fails in staging. === 6.3 Interfaces as Black Boxes @@ -219,7 +219,7 @@ State is almost ubiquitous, and practically a synonym of applications, because a When a request results in a long running job (such as sending out an email campaign, modifying records in a persistant database, etc), it's best to hand that off into a separate service that -- again -- mostly keeps state regarding said job. Separating services into specific needs means we can keep web servers lean, stateless, and improve our flows by adding more servers, persistent queues (so that we don't drop jobs), and so on. When every task is tethered together through tight coupling and state, it could become challenging to maintain, upgrade, and scale a service over time. -Derived state in the form of caches is not uncmoon in the world of web servers. In the case of a personal website with some books available for download, for instance, we might be tempted to store the PDF representation of each book in a file, so that we don't have to recompile the PDF whenever the corresponding `/book` route is visited. When the book is updated, we'd recompute the PDF file and flush it to disk again, so that this derived state remains fresh. When our web server ceases to be single-node and we start using a cluster of several nodes, however, it might not be so trivial to broadcast the news about books being updated across nodes, and thus it'd be best to leave derived state to the persistance layer. Otherwise, a web server node might receive the request to update a book, perform the update and recompute the PDF file on that node, but we’d be failing to invalidate the PDF files being served by other nodes, which would have, and continue to serve stale copies of the PDF representation. +Derived state in the form of caches is not uncommon in the world of web servers. In the case of a personal website with some books available for download, for instance, we might be tempted to store the PDF representation of each book in a file, so that we don't have to recompile the PDF whenever the corresponding `/book` route is visited. When the book is updated, we'd recompute the PDF file and flush it to disk again, so that this derived state remains fresh. When our web server ceases to be single-node and we start using a cluster of several nodes, however, it might not be so trivial to broadcast the news about books being updated across nodes, and thus it'd be best to leave derived state to the persistance layer. Otherwise, a web server node might receive the request to update a book, perform the update and recompute the PDF file on that node, but we’d be failing to invalidate the PDF files being served by other nodes, which would have, and continue to serve stale copies of the PDF representation. A better alternative in such a case would be to store derived state in a data store like Redis or Amazon S3, either of which we could update from any web server, and then serving precomputed results from Redis directly. In this way we'd still be able to access the latency benefits of using precomputed derived state, but at the same time we'd stay resilient when these requests or updates can happen on multiple web server nodes. @@ -268,13 +268,13 @@ Proper integration testing might catch many of these kinds of mistakes, but that Eager abstraction can result in catastrophe. Conversely, failure to identify and abstract away sources of major complexity can be incredibly costly as well. When we consume complex interfaces directly, but don't necessarily take advantage of all the advanced configuration options that interface has to offer, we are missing out on a powerful abstraction we could be using. The alternative would be to create a middle layer in front of the complex interface, and have consumers go through that layer instead. -This intermediate layer would be in charge of calling the complex abstraction itself, but offers a simpler interface with less configuration options and improved ease of use for the use cases that matter to us. Often, complicated or legacy interfaces demand that we offer up data that could be derived from other parameters being passed into the function call. For example, we might be asked how many adults, how many children, and how many people in total are looking to make a flight booking, even though the latter can be derived from the former. Other examples include expecting fields to be in a particular string format (such as a date string that could be derived from a native JavaScript date instead), using nomenclature that's relevant to the implmentation but not so much to the consumer, or a lack of sensible defaults (required fields which are rarely changed into anything other than a recommended value that isn't set by default). +This intermediate layer would be in charge of calling the complex abstraction itself, but offers a simpler interface with less configuration options and improved ease of use for the use cases that matter to us. Often, complicated or legacy interfaces demand that we offer up data that could be derived from other parameters being passed into the function call. For example, we might be asked how many adults, how many children, and how many people in total are looking to make a flight booking, even though the latter can be derived from the former. Other examples include expecting fields to be in a particular string format (such as a date string that could be derived from a native JavaScript date instead), using nomenclature that's relevant to the implementation but not so much to the consumer, or a lack of sensible defaults (required fields which are rarely changed into anything other than a recommended value that isn't set by default). When we're building out a web application which consumes a highly parametized API in order to search for the cheapest hassle-free flights -- to give an example -- and we anticipate consuming this API in a few different ways, it would cost us dearly not to abstract away most of the parameters demanded by the API which do not fit our use case. This middle layer can take care of establishing sensible default values and of converting reasonable data structures such as native JavaScript dates or case insensitive airport codes into the formats demanded by the API we're using. In addition, our abstraction could also take care of any follow up API calls that need to be made in order to hydrate data. For example, a flight search API might return an airline code for each different flight, such as AA for American Airlines, but a UI consumer would also necessitate to hydrate AA into a display name for the airline, accompanied by a logo to embed on the user interface, and perhaps even a quick link to their check-in page. -When we call into the backing API every time, with the full query, appeasing its quirks and shortcomings instead of taking the abstracted approach, it will not only be difficult to maintain an application that consumes those endpoints in more than one place, but it will also become a challenge down the road, when we want to include results from a different provider, which of course would have their own set of quirks and shortcomings. At this point we would have two separate sets of API calls, one for each provider, and each massaging the data to accomodate provider-specific quirks in a module which shouldn't be concerned with such matters, but only the results themselves. +When we call into the backing API every time, with the full query, appeasing its quirks and shortcomings instead of taking the abstracted approach, it will not only be difficult to maintain an application that consumes those endpoints in more than one place, but it will also become a challenge down the road, when we want to include results from a different provider, which of course would have their own set of quirks and shortcomings. At this point we would have two separate sets of API calls, one for each provider, and each massaging the data to accommodate provider-specific quirks in a module which shouldn't be concerned with such matters, but only the results themselves. A middle layer could leverage a normalized query from the consumer, such as the one where we took a native date and then format it when calling the flight search API, and then adapt that query into either of the backing services that actually produce flight search results. This way, the consumer only has to deal with a single, simplified interface, while having the ability to seamlessly interact with two similar backing services that offer different interfaces. From db428588657e14aaf8aeb86432bd28582a5169d4 Mon Sep 17 00:00:00 2001 From: Tiago Hermano Date: 2018年9月30日 20:02:40 -0300 Subject: [PATCH 2/9] fixed grammar typo changed 'ellapses' to 'elapses' --- chapters/ch04.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ch04.asciidoc b/chapters/ch04.asciidoc index 5f46bcf..bd40f49 100644 --- a/chapters/ch04.asciidoc +++ b/chapters/ch04.asciidoc @@ -719,6 +719,6 @@ Splitting logic into a few files under the same directory helps us prevent an ex The alternative, placing logic related to a particular aspect of our application such as blog posts directly in the components where it's needed, will cause trouble if left unchecked. Doing so might be beneficial in terms of short-term productivity, but longer-term we need to worry about coupling logic, strictly related to blog posts in this case, together with entirely different concerns. At the same time, if we sprinkle a bulk of the logic across several unrelated components, we become at risk of missing critical aspects of functionality when making large-scale updates to the codebase, and because of this we might end up making the wrong assumptions, or mistakes that only become evident much further down the line. -It's acceptable to start out placing logic directly where it's needed at first, when it's unclear whether the functionality will grow or how much. Once this initial exploratory period ellapses, and it becomes clear the functionality is here to stay and more might be to come, it's advisable that we isolate the functionality for the reasons stated above. Later, as the functionality grows in size and in concerns that need to be addressed, we can componentize each aspect into different modules that are still grouped together logically in the file system, making it easy to take all of interrelated concerns into account when need be. +It's acceptable to start out placing logic directly where it's needed at first, when it's unclear whether the functionality will grow or how much. Once this initial exploratory period elapses, and it becomes clear the functionality is here to stay and more might be to come, it's advisable that we isolate the functionality for the reasons stated above. Later, as the functionality grows in size and in concerns that need to be addressed, we can componentize each aspect into different modules that are still grouped together logically in the file system, making it easy to take all of interrelated concerns into account when need be. Now that we have broken down the essentials of module design and how to delineate interfaces, as well as how to lockdown, isolate, and drive down complexity in our internal implementations, we're ready to start discussing JavaScript-specific language features and an assortment of patterns that we can benefit from. From 74c6b8b4256ae9331fa3c58369b66b31ada2b20f Mon Sep 17 00:00:00 2001 From: Mindaugas Macevicius Date: Mon, 1 Oct 2018 12:36:49 +0200 Subject: [PATCH 3/9] fix typo --- chapters/ch02.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ch02.asciidoc b/chapters/ch02.asciidoc index 2afea77..e27287b 100644 --- a/chapters/ch02.asciidoc +++ b/chapters/ch02.asciidoc @@ -353,7 +353,7 @@ Humans excell at identifying patterns, and we do so while reading as well. That' When a set of functions has the same API shape, consumers can intuitively deduce how the next function is used. Consider the native `Array`, where `#forEach`, `#map`, `#filter`, `#find`, `#some`, and `#every` all accept a callback as their first parameter and optionally take the context when calling that callback as their second parameter. Further, the callback receives the current `item`, that item's `index`, and the `array` itself as parameters. The `#reduce` and `#reduceRight` methods are a little different in that the callback receives an `accumulator` parameter in the first position, but then it goes on to receive the current `item`, that item's `index`, the `array`, making the shape quite similar to what we are accustomed to. -The result is we rarely need to reach for documentation in order to understand how these functions are shaped. The difference lies solely in how the consumer-provided callback is used, and what the return value for the method is. `#forEach` doesn't return a value. `#map` returns the result of each invocation, `#filter` returns only the items for which the callback returned a truthy value. `#some` returns `false` unless the callback returns a truthy value for one of the items, in which case it returns `true` and breaks out of the look. `#every` returns `false` unless the callback returns a truthy value for every item, in which case it returns `true`. +The result is we rarely need to reach for documentation in order to understand how these functions are shaped. The difference lies solely in how the consumer-provided callback is used, and what the return value for the method is. `#forEach` doesn't return a value. `#map` returns the result of each invocation, `#filter` returns only the items for which the callback returned a truthy value. `#some` returns `false` unless the callback returns a truthy value for one of the items, in which case it returns `true` and breaks out of the loop. `#every` returns `false` unless the callback returns a truthy value for every item, in which case it returns `true`. When we have different shapes for functions that perform similar tasks, we need to make an effort to remember each individual function's shape instead of being able to focus on the task at hand. Consistency is valuable on every level of a codebase: consistent code style reduces friction among developers and conflicts when merging code, consistent shapes optimize readability and give way to intuition, consistent naming and architecture reduces surprises and keeps code uniform. From 6ab1e1ccc5d2cd1833f241eb0e60a37b4f895bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Dammaretz?= Date: Thu, 4 Oct 2018 11:43:12 +0200 Subject: [PATCH 4/9] Fix chapter 2 forgotten await Body.json() return a Promise. WIthout the await there, it would log a pending Promise. https://developer.mozilla.org/en-US/docs/Web/API/Body/json --- chapters/ch02.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ch02.asciidoc b/chapters/ch02.asciidoc index e27287b..42a5918 100644 --- a/chapters/ch02.asciidoc +++ b/chapters/ch02.asciidoc @@ -464,7 +464,7 @@ If we want to take things a bit further, we can chain onto the response object t [source,javascript] ---- const res = await fetch('/api/users/john') -const data = res.json() +const data = await res.json() console.log(data.name) // <- 'John Doe' ---- From f3a355afda8f4d87a19e4b65aafcb08ca037b493 Mon Sep 17 00:00:00 2001 From: Mindaugas Macevicius <40419643+api-xplr@users.noreply.github.com> Date: 2018年10月10日 11:41:13 +0200 Subject: [PATCH 5/9] fixed typos and inacurracies (#17) * added space between words * correct function name * fix typo * Reverted --- chapters/ch04.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chapters/ch04.asciidoc b/chapters/ch04.asciidoc index bd40f49..d4b4ed2 100644 --- a/chapters/ch04.asciidoc +++ b/chapters/ch04.asciidoc @@ -48,7 +48,7 @@ When we'd have to repeat ourselves by passing a lot of scope variables as functi ==== 4.1.3 Frameworks: the Good the Bad and the Ugly -Conventions are useful because they allow for better self-direction amongst developers, without causing lagoons of inconsistency to spread across our codebase as fate would have it were we to allow a team of developers too much freedom without sound design direction and conventions that dictatehow different portions of an application should be shaped. A large number of conventions might hinder productivity, especially if some of our conventions appeared to work as if by magic. +Conventions are useful because they allow for better self-direction amongst developers, without causing lagoons of inconsistency to spread across our codebase as fate would have it were we to allow a team of developers too much freedom without sound design direction and conventions that dictate how different portions of an application should be shaped. A large number of conventions might hinder productivity, especially if some of our conventions appeared to work as if by magic. When it comes to conventions, frameworks are a special case. Frameworks are packed to the brim with conventions and best practices. Some of them live in the library and tooling ecosystem around the framework, while many live in the shape our code takes when we rely on said framework. Upon adopting a framework, we're buying into its conventions and practices. Most modern JavaScript frameworks offer ways of breaking our application into small chunks, regardless of whether the framework is for the client or server. @@ -320,7 +320,7 @@ a(function () { }) ---- -The foremost problem with this kind of structure is scope inheritance. In the deepest callback, passed to the `g` function, we've inherited the combined scopes of all the parent callbacks. As functions become larger, and more variables are bound into each of these scopes, it becomes ever more challenging to understand one of the callbacks in isolation from its parents. +The foremost problem with this kind of structure is scope inheritance. In the deepest callback, passed to the `d` function, we've inherited the combined scopes of all the parent callbacks. As functions become larger, and more variables are bound into each of these scopes, it becomes ever more challenging to understand one of the callbacks in isolation from its parents. This kind of coupling can be reverted by naming the callbacks and placing them all in the same nesting level. Named functions may be reused in other parts of our component, or exported to be used elsewhere. In the following example we've eliminated up to 3 levels of unnecessary nesting, and by eliminating nesting we've made the scope for each function more explicit. From d768a1ef308f00a69d050a76bf2ff08d36a8de70 Mon Sep 17 00:00:00 2001 From: Nick Rance Date: 2018年10月16日 02:25:57 +0100 Subject: [PATCH 6/9] typos (#19) typos --- chapters/ch02.asciidoc | 2 +- chapters/ch03.asciidoc | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chapters/ch02.asciidoc b/chapters/ch02.asciidoc index 42a5918..13051af 100644 --- a/chapters/ch02.asciidoc +++ b/chapters/ch02.asciidoc @@ -446,7 +446,7 @@ On the same vein, we should avoid optional input parameters which transform the It isn't necessary to treat failure and success with the same response shape, meaning that failure results can always be `null` or `undefined`, while success results might be an array list. However, consistency should be required across all failure cases and across all sucess cases, respectively. -Having consistent data types mitigates surpises and improves the confidence a consumer has in our API. +Having consistent data types mitigates surprises and improves the confidence a consumer has in our API. ==== 2.2.4 Simplicity diff --git a/chapters/ch03.asciidoc b/chapters/ch03.asciidoc index 5f8469c..732eb25 100644 --- a/chapters/ch03.asciidoc +++ b/chapters/ch03.asciidoc @@ -1,7 +1,7 @@ [[module-design]] == Module Design -Thinking in terms of API-driven and documentation-driven design will yield more usable modules than not doing so. You might argue that internals are not that important: "as long as the interface holds, we can put anything we want in the mix!". A usable interface is only one side of the equation; it will do little to keep the maintainability of your applications in check. Properly designed module internals help keep our code readable and its intent clear. In this chapter we'll debate what it takes to write modules with scalability in mind, but without getting too far ahead of our current requirements. We'll discuss the CRUST constraints in some more depth, and finally ellaborate on how to prune modules as they become larger and more complex over time. +Thinking in terms of API-driven and documentation-driven design will yield more usable modules than not doing so. You might argue that internals are not that important: "as long as the interface holds, we can put anything we want in the mix!". A usable interface is only one side of the equation; it will do little to keep the maintainability of your applications in check. Properly designed module internals help keep our code readable and its intent clear. In this chapter we'll debate what it takes to write modules with scalability in mind, but without getting too far ahead of our current requirements. We'll discuss the CRUST constraints in some more depth, and finally elaborate on how to prune modules as they become larger and more complex over time. === 3.1 Growing a Module @@ -111,7 +111,7 @@ Even when the smaller component isn't being reused anywhere else, and perhaps no Chopping up internals doesn't merely only work for view components and their children. That said, view components pose a great example that might help us visualize how complexity can remain flat across a component system, regardless of how deep we go, instead of being contained in a large component with little structure and a high-level of complexity or coupling. This is akin to looking at the universe on a macroscopic level and then taking a closer look, until we get to the atomic level, and then beyond. Each layer has its own complexities and intricacies waiting to be discovered, but the complexity is spread across the layers rather than clustered on any one particular layer. The spread reduces the amount of complexity we have to observe and deal with on any given layer. -Speaking of layers, it is at this stage of the design process that you might want to consider defining different layers for your application. You might be used to having models, views, and controllers in MVC applications, or maybe you're accustomed to actions, reducers, and selectors in Redux applications. Maybe you should think of implemeting a service layer where all the business logic occurs, or perhaps a persistance layer where all the caching and persistent storage takes place. +Speaking of layers, it is at this stage of the design process that you might want to consider defining different layers for your application. You might be used to having models, views, and controllers in MVC applications, or maybe you're accustomed to actions, reducers, and selectors in Redux applications. Maybe you should think of implementing a service layer where all the business logic occurs, or perhaps a persistance layer where all the caching and persistent storage takes place. When we're not dealing with modules which we ought to shape in a certain way, like views, but modules that can be composed any which way we choose, like services, we should consider whether new features belong in an existing module or in an entirely new module. When we have a module which wraps a Markdown parsing library adding functionality such as support for emoji expansions, and want an API that can take the resulting HTML and strip out certain tags and attributes, should we add that functionality to the Markdown module or put it in a separate module? @@ -155,11 +155,11 @@ We can't prevent this from happening over and over -- not entirely. Unexpected b What we can do is mitigate the risk of bugs by writing more predictable code or improving test coverage. We can also become more proficient at debugging. -On the predictable code arena, we must be sure to handle every expected error. When it comes to error handling we typically will bubble the error up the stack and handle it at the top, by logging it to an analytics tracker, to standard output, or to a database. When using a function call we know might throw, like `JSON.parse` on user input, we should wrap it with `try`/`catch` and handle the error, again bubbling it up to the consumer if our inability to proceed with the function logic is final. If we're dealing with conventional callbacks that have an error argument, let's handle the error in a guard clause. Whenever we have a promise chain, make sure to add a `.catch` reaction to the end of the chain that handles any errors ocurring in the chain. In the case of `async` functions, we could use `try`/`catch` or, alternatively, we can also add a `.catch` reaction to the result of invoking the async function. While leveraging streams or other conventional event-based interfaces, make sure to bind an `error` event handler. Proper error handling should all but eliminate the chance of expected errors crippling our software. Simple code is predictable. Thus, following the suggestions in chapter 4 will aid us in reducing the odds of encountering unexpected errors as well. +On the predictable code arena, we must be sure to handle every expected error. When it comes to error handling we typically will bubble the error up the stack and handle it at the top, by logging it to an analytics tracker, to standard output, or to a database. When using a function call we know might throw, like `JSON.parse` on user input, we should wrap it with `try`/`catch` and handle the error, again bubbling it up to the consumer if our inability to proceed with the function logic is final. If we're dealing with conventional callbacks that have an error argument, let's handle the error in a guard clause. Whenever we have a promise chain, make sure to add a `.catch` reaction to the end of the chain that handles any errors occurring in the chain. In the case of `async` functions, we could use `try`/`catch` or, alternatively, we can also add a `.catch` reaction to the result of invoking the async function. While leveraging streams or other conventional event-based interfaces, make sure to bind an `error` event handler. Proper error handling should all but eliminate the chance of expected errors crippling our software. Simple code is predictable. Thus, following the suggestions in chapter 4 will aid us in reducing the odds of encountering unexpected errors as well. Test coverage can help detect unexpected errors. If we have simple and predictable code, it's harder for unexpected errors to seep through the seams. Tests can further abridge the gap by enlarging the corpus of expected errors. When we add tests, preventable errors are codified by test cases and fixtures. When tests are comprehensive enough, we might run into unexpected errors in testing and fix them. Since we've already codified them in a test case, these errors can't happen again (a test regression) without our test suite failing. -Regardless of how determined we are to develop simple, predictable, and throughly tested programs, we're still bound to run into bugs we hadn't expected. Tests exist mostly to prevent regressions, preventing us from running once again into bugs we've already fixed; and to prevent expected mistakes, errors we think might arise if we were to tweak our code in incorrect ways. Tests can do little to prognosticate and prevent software bugs from happening, however. +Regardless of how determined we are to develop simple, predictable, and thoroughly tested programs, we're still bound to run into bugs we hadn't expected. Tests exist mostly to prevent regressions, preventing us from running once again into bugs we've already fixed; and to prevent expected mistakes, errors we think might arise if we were to tweak our code in incorrect ways. Tests can do little to prognosticate and prevent software bugs from happening, however. This brings us to the inevitability of debugging. Using step-through debugging and inspecting application state as we step through the code leading to a bug is an useful tool, but it will not help us debug our code any faster than we can diagnose exactly what is going on. From 0e4544b404f19ce26047d414f7636841380160e1 Mon Sep 17 00:00:00 2001 From: Andrey Pelykh <10947577+andreypelykh@users.noreply.github.com> Date: 2018年10月30日 16:40:36 +0200 Subject: [PATCH 7/9] Update ch04.asciidoc (#20) --- chapters/ch04.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ch04.asciidoc b/chapters/ch04.asciidoc index d4b4ed2..dfea173 100644 --- a/chapters/ch04.asciidoc +++ b/chapters/ch04.asciidoc @@ -108,7 +108,7 @@ We could've used more variables without creating a function, inlining the comput [source,javascript] ---- -const hasToken = auth === undefined || auth.token === undefined +const hasToken = auth !== undefined && auth.token !== undefined const hasValidToken = hasToken && auth.expires> Date.now() if (hasValidToken) { return From 78d23dd0bc4fe9b7e95c70c66c3786381a3e2709 Mon Sep 17 00:00:00 2001 From: Milan Misak <1205297+milanmisak@users.noreply.github.com> Date: 2019年1月17日 18:45:42 +0000 Subject: [PATCH 8/9] Fix a few chapter 4 typos (#21) --- chapters/ch04.asciidoc | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/chapters/ch04.asciidoc b/chapters/ch04.asciidoc index dfea173..d9c12ba 100644 --- a/chapters/ch04.asciidoc +++ b/chapters/ch04.asciidoc @@ -56,7 +56,7 @@ Express has middleware and routes, Angular has directives, services, and control Eventually, we'll come into requirements that don't exactly fit the mold proposed by our framework of choice. Generally, this means the required functionality belongs on a separate layer. For example, Express in Node.js is a framework concerned with handling HTTP requests and serving responses. If one of our API endpoints needs to result in an email being sent, we could embed email-sending logic in the controller for that API endpoint. However, if an API endpoint controller is already concerned with, say, publishing blog posts, then it would be hardly right to embed email-sending logic on that same controller, since it's a different concern entirely. Instead, what we could do is create a `subscribers` service component, with functionality such as `subscribe` which adds a subscriber after verifying their email, and `notify` which takes care of sending the emails. Taking this idea further still, perhaps most of the work in `subscribers.notify` should occur via yet another service component called `emails`, which takes care of properly configuring our email sending capability, and also has functionality to turn would-be emails into plain `console.log` statements for quick access to the contents of the emails during debug sessions. -Having clearly defined layers is paramount to the design of effective and maintainable applications once we're past the prototyping stages. Layers can be made up of components which follow the conventions proposed by the frameworks we use, or they can be self-imposed like the service layer we discussed in the previous paragraph. Using layers, and as long as we favor function parameters over scope for context-passing, we can introduce horizonal scaling by placing several orthogonal components alongside each other, without letting them run into each others' concerns. +Having clearly defined layers is paramount to the design of effective and maintainable applications once we're past the prototyping stages. Layers can be made up of components which follow the conventions proposed by the frameworks we use, or they can be self-imposed like the service layer we discussed in the previous paragraph. Using layers, and as long as we favor function parameters over scope for context-passing, we can introduce horizontal scaling by placing several orthogonal components alongside each other, without letting them run into each others' concerns. === 4.2 Refactoring Complex Code @@ -143,7 +143,7 @@ if (response) { In the example, we're optimizing readability for the success case, while the failure handling is relegated to the very end of our piece of code. There's several problems with this approach. For one, we have to indulge in unnecessary nesting of every success condition, or otherwise put them all in a huge conditional statement. While it's rather easy to understand the success case, things can get tricky when we're trying to debug programs like this, as we need to keep the conditionals in our head the whole time we're reading the program. -A better alternative is to flip the conditionals, placing all failure handling statements near the top. While counterintuitive at first, this approach has several benefits. It reduces nesting and eliminates `else` branches while promoting failure handling to the top of our code, and this has the added benefit that we'll become more aware of error handling and naturally gravitate towards thinking about the failure cases first, a great trait to have when doing application development, where forgetting to handle a failure case might result in an incosistent experience for end users with a hard-to-trace error on top. +A better alternative is to flip the conditionals, placing all failure handling statements near the top. While counterintuitive at first, this approach has several benefits. It reduces nesting and eliminates `else` branches while promoting failure handling to the top of our code, and this has the added benefit that we'll become more aware of error handling and naturally gravitate towards thinking about the failure cases first, a great trait to have when doing application development, where forgetting to handle a failure case might result in an inconsistent experience for end users with a hard-to-trace error on top. [source,javascript] ---- @@ -162,7 +162,7 @@ Another benefit of guard clauses is almost implicit: given that they're placed n Guard clauses don't tell the reader everything they need to know that might go wrong when calling a function, but they give them a peek into expected immediate failure cases. Other things that might go wrong lie in the implementation details of the function. Perhaps we use a different service or library to fulfill the bulk of our function's task, and that service or library comes with its own set of nested guard clauses and potential failure cases that will bubble up all the way to our own function's outcome. -==== 4.2.3 An Interdependency Piramid +==== 4.2.3 An Interdependency Pyramid Writing straightforward code is not all that different from writing other straightforward texts. Texts are often arranged in paragraphs, which are somewhat comparable with functions: we can consider their input to be the reader's knowledge and everything else they've read so far in the text, and the output can be what the reader gets out of the paragraph. @@ -237,7 +237,7 @@ function getUserModels(done) { } ---- -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. +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 containing 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. [source,javascript] ---- @@ -478,7 +478,7 @@ A similar situation occurs when we have a concurrent flow that remains more or l In other cases, we might notice how there's a few different components that all need the same piece of functionality. Commenting features often fall in this case, where different components like user profiles, projects, or artifacts, might need the ability to receive, show, edit, and delete comments. This case can be interesting because it's the business requirement is not always identified upfront, and we might embed the child feature into the parent component before realizing it'd be useful to extract the feature so that it can be reused in other parent components. While this sounds obvious in hindsight, it's not always clear when we'll need to reuse some functionality somewhere else, and keeping every aspect of functionality isolated just in case we need to reuse them can be costly in terms of time and development effort. -More often than not, however, abstractions can end up complicating matters. It might be that the tradeoff isn't worth it because the code becomes much harder to read, or maybe because the underlying code isn't mature enough yet, or we don't know what special requirements we may end up with for other objects adopting similar functionality, meaning we're not comfortable creating an abstraction that could lead to unforeseen problems in the future. +More often than not, however, abstractions can end up complicating matters. It might be that the trade-off isn't worth it because the code becomes much harder to read, or maybe because the underlying code isn't mature enough yet, or we don't know what special requirements we may end up with for other objects adopting similar functionality, meaning we're not comfortable creating an abstraction that could lead to unforeseen problems in the future. Whenever we are uncertain about whether an abstraction is up to muster, it pays to go back to the original piece of code we had before introducing the abstraction, and comparing the two pieces. Is the new piece easier to understand, modify, and consume? Would that still be the case as a newcomer? Try and consider how the outcome to those questions would change if you hadn't looked at this code in a while. Ask your co-workers for their opinion, too; given they haven't seen that code yet and they may end up having to consume it, they're great candidates to help decide which approach is better. @@ -510,15 +510,15 @@ At its heart, state is mutable. Even if the variable bindings themselves are imm Consider a game of chess, where each of two players starts with 16 pieces, each deterministically assigned a position on a checkerboard. The initial state is always the same. As each player inputs their actions, moving and trading pieces, the system state mutates. A few moves into the game, there is a good chance we'll be facing a game state we haven't ever experienced before. Computer program state is a lot like a game of chess, except there's more nuance in the way of user input, and an infinitude of possible board positions and state permutations. -In the world of web development, a human decides to open a new tab in their favorite web browser and they then google for "cat in a pickle gifs". The browser allocates a new process through a system call to the operating system, which shifts some bits around on the physical hardware that lies inside the human’s computer. Before the HTTP request hits the network, we need to hit DNS servers, engaging in the elaborate process of casting `google.com` into an IP address. The browser then checks whether there’s a ServiceWorker installed, and assuming there isn’t one the request finally takes the default route of querying Google’s servers for the phrase "cat in a pickle gifs". +In the world of web development, a human decides to open a new tab in their favorite web browser and they then google for "cat in a pickle gifs". The browser allocates a new process through a system call to the operating system, which shifts some bits around on the physical hardware that lies inside the human’s computer. Before the HTTP request hits the network, we need to hit DNS servers, engaging in the elaborate process of casting `google.com` into an IP address. The browser then checks whether there's a ServiceWorker installed, and assuming there isn't one the request finally takes the default route of querying Google’s servers for the phrase "cat in a pickle gifs". Naturally, Google receives this request at one of the front-end edges of its public network, in charge of balancing the load and routing requests to healthy back-end services. The query goes through a variety of analyzers that attempt to break it down to its semantic roots, stripping the query down to its essential keywords in an attempt to better match relevant results. The search engine figures out the 10 most relevant results for "cat pickle gif" out of billions of pages in its index – which was of course primed by a different system that’s also part of the whole – and at the same time, Google pulls down a highly targeted piece of relevant advertisement about cat gifs that matches what they believe is the demographic the human making the query belongs to, thanks to a sophisticated ad network that figures out whether the user is authenticated with Google through an HTTP header session cookie and the search results page starts being constructed and streamed to the human, who now appears impatient and fidgety. -As the first few bits of HTML being streaming down the wire, the search engine produces its results and hands them back to the front-end servers, which includes it in the HTML stream that’s sent back to the human. The web browser has been working hard at this too, parsing the incomplete pieces of HTML that have been streaming down the wire as best it could, even daring to launch other admirably and equally-mind-boggling requests for HTTP resources presumed to be JavaScript, CSS, font, and image files as the HTML continues to stream down the wire. The first few chunks of HTML are converted into a DOM tree, and the browser would finally be able to begin rendering bits and pieces of the page on the screen, weren't it for the pending, equally-mind-boggling CSS and font requests. +As the first few bits of HTML being streaming down the wire, the search engine produces its results and hands them back to the front-end servers, which includes it in the HTML stream that's sent back to the human. The web browser has been working hard at this too, parsing the incomplete pieces of HTML that have been streaming down the wire as best it could, even daring to launch other admirably and equally-mind-boggling requests for HTTP resources presumed to be JavaScript, CSS, font, and image files as the HTML continues to stream down the wire. The first few chunks of HTML are converted into a DOM tree, and the browser would finally be able to begin rendering bits and pieces of the page on the screen, weren't it for the pending, equally-mind-boggling CSS and font requests. -As the CSS stylesheets and fonts are transmitted, the browser begins modeling the CSSOM and getting a more complete picture of how to turn the HTML and CSS plain text chunks provided by Google servers into a graphical representation that the human finds pleasant. Browser extensions get a chance to meddle with the content, removing the highly targeted piece of relevant advertisement about cat gifs before I even realize Google hoped I wouldn’t block ads this time around. +As the CSS stylesheets and fonts are transmitted, the browser begins modeling the CSSOM and getting a more complete picture of how to turn the HTML and CSS plain text chunks provided by Google servers into a graphical representation that the human finds pleasant. Browser extensions get a chance to meddle with the content, removing the highly targeted piece of relevant advertisement about cat gifs before I even realize Google hoped I wouldn't block ads this time around. A few seconds have passed by since I first decided to search for cat in a pickle gifs. Needless to say, thousands of others brought similarly inane requests. To the same systems. During this time. @@ -526,13 +526,13 @@ Not only does this example demonstrate the marvelous machinery and infrastructur ==== 4.3.2 Eliminating Incidental State -We've established that the overall state of a system has little to do with our ability to comprehend parts of that same system. Our focus in reducing state-based entropy must then lie in the individual aspects of the system. It’s for this reason that breaking apart large pieces of code is so effective. We’re reducing the amount of state local to each given aspect of the system, and that’s the kind of state that’s worth taking care of, since it’s what we can keep in our heads and make sense of. +We've established that the overall state of a system has little to do with our ability to comprehend parts of that same system. Our focus in reducing state-based entropy must then lie in the individual aspects of the system. It's for this reason that breaking apart large pieces of code is so effective. We’re reducing the amount of state local to each given aspect of the system, and that's the kind of state that's worth taking care of, since it's what we can keep in our heads and make sense of. -Whenever there's persistance involved, there's going to be a discrepancy between ephemeral state and realized state. In the case of a web application, we could define ephemeral state as any user input that hasn't resulted in state being persisted yet, as might be the case of an unsaved user preference that might be lost unless persisted. We can say realized state is the state that has been persisted, and that different programs might have different strategies on how to convert ephemeral state into realized state. A web application might adopt an Offline-First pattern where ephemeral state is automatically synchronized to an IndexedDB database in the browser, and eventually realized by updating the state persisted on a back-end system. When the Offline-First page is reloaded, unrealized state may be pushed to the back-end or discarded. +Whenever there's persistence involved, there's going to be a discrepancy between ephemeral state and realized state. In the case of a web application, we could define ephemeral state as any user input that hasn't resulted in state being persisted yet, as might be the case of an unsaved user preference that might be lost unless persisted. We can say realized state is the state that has been persisted, and that different programs might have different strategies on how to convert ephemeral state into realized state. A web application might adopt an Offline-First pattern where ephemeral state is automatically synchronized to an IndexedDB database in the browser, and eventually realized by updating the state persisted on a back-end system. When the Offline-First page is reloaded, unrealized state may be pushed to the back-end or discarded. Incidental state can occur when we have a piece of data that's used in several parts of an application, and which is derived from other pieces of data. When the original piece of data is updated, it wouldn't be hard to inadvertently leave the derived pieces of data in their current state, making them stale when compared to the updated original pieces of data. As an example, consider a piece of user input in Markdown and the HTML representation derived from that piece of Markdown. If the piece of Markdown is updated but the previously compiled pieces of HTML are not, then different parts of the system might display different bits of HTML out of what was apparently the same single Markdown source. -When we persist derived state, we're putting the original and the derived data at risk of falling out of sync. This isn't the case just when dealing with persistance layers, but can also occur in a few other scenarios as well. When dealing with caching layers, their content may become stale because the underlying original piece of content is updated but we forget to invalidate pieces of content derived from the updated data. Database denormalization is another common occurrence of this problem, whereby creating derived state can result in synchronization problems and stale byproducts of the original data. +When we persist derived state, we're putting the original and the derived data at risk of falling out of sync. This isn't the case just when dealing with persistence layers, but can also occur in a few other scenarios as well. When dealing with caching layers, their content may become stale because the underlying original piece of content is updated but we forget to invalidate pieces of content derived from the updated data. Database denormalization is another common occurrence of this problem, whereby creating derived state can result in synchronization problems and stale byproducts of the original data. This lack of synchronization is often observed in discussion forum software, where user profiles are denormalized into comment objects in an effort to save a database roundtrip. When users update later update their profile, however, their old comments preserve an stale avatar, signature, or display name. To avoid this kind of issue, we should always consider recomputing derived state from its roots. Even though doing so won't always be possible, performant, or even practical, encouraging this kind of thinking across a development team will, if anything, increase awareness about the subtle intricacies of denormalized state. @@ -542,7 +542,7 @@ As long as we're aware of the risks of data denormalization, we can then indulge State is inevitable. As we discussed in section 4.3.1, though, the full picture hardly affects our ability to maintain small parts of that state tree. In the local case -- each of the interrelated but ultimately separate pieces of code we work with in our day to day -- all that matters are the inputs we receive and the outputs we produce. That said, generating a large amount of output where we could instead emit a single piece of information is undesirable. -When all intermediate state is contained inside a component instead of being leaked to others, we're reducing the friction in interacting with our component or function. The more we condense state into its smallest possible representation for output purposes, the better contained our functions will become. Incidentaly, we're making the interface easier to consume. Since there's less state to draw from, there's fewer ways of consuming that state. This reduces the amount of possible use cases, but by favoring composability over serving every possible need, we're making each piece of functionality, when evaluated on its own, simpler. +When all intermediate state is contained inside a component instead of being leaked to others, we're reducing the friction in interacting with our component or function. The more we condense state into its smallest possible representation for output purposes, the better contained our functions will become. Incidentally, we're making the interface easier to consume. Since there's less state to draw from, there's fewer ways of consuming that state. This reduces the amount of possible use cases, but by favoring composability over serving every possible need, we're making each piece of functionality, when evaluated on its own, simpler. One other case where we may incidentally increase complexity is whenever we modify the property values of an input. This type of operation should be made extremely explicit, as to not be confused, and avoided where possible. If we assume functions to be defined as the equation between the inputs we receive and the outputs we produce, then the side-effects are ill-advised. Mutations on the input within the body of a function is one example of side-effects, which can be a source of bugs and confusion, particularly due to the difficulty in tracking down the source for these mutations. @@ -703,7 +703,7 @@ console.log(multiply(add(5, 3), 2)) // <- 16 Taking this concept beyond basic mathematics, we can begin to see how this decoupling of form and function, or state and logic, can be increasingly beneficial. It's easier to serialize plain data over the wire, keep it consistent across different environments, and make it interoperable regardless of the logic, than if we tightly coupled data and the logic around it. -Functions are, to a certain degree, hopelessly coupled to the data they receive as inputs: in order for the function to work as expected, the data it receives must satisfy its contract for that piece of input. Within the bounds of a function's proper execution, the data must have a certain shape, traits, or adhere to whatever restrictions the function has in place. These restrictions may be somewhat lax (e.g "must have a `toString` method"), highly specific (e.g "must be a function that accepts 3 arguments and returns a decimal number between 0 and 1"), or anywhere in between. A simple interface is usually highly restrictive (e.g accepting only a boolean value). Meanwhile, it's not uncommon for loose interfaces to become burdened by their own flexibility, leading to complex implementations that attempt to accomodate many different shapes and sizes of the same input parameter. +Functions are, to a certain degree, hopelessly coupled to the data they receive as inputs: in order for the function to work as expected, the data it receives must satisfy its contract for that piece of input. Within the bounds of a function's proper execution, the data must have a certain shape, traits, or adhere to whatever restrictions the function has in place. These restrictions may be somewhat lax (e.g. "must have a `toString` method"), highly specific (e.g. "must be a function that accepts 3 arguments and returns a decimal number between 0 and 1"), or anywhere in between. A simple interface is usually highly restrictive (e.g. accepting only a boolean value). Meanwhile, it's not uncommon for loose interfaces to become burdened by their own flexibility, leading to complex implementations that attempt to accommodate many different shapes and sizes of the same input parameter. We should aim to keep logic restrictive and only as flexible as deemed necessary by business requirements. When an interface starts out being restrictive we can always slowly open it up later as new use cases and requirements arise, but by starting out with a small use case we're able to grow the interface into something that's naturally better fit to handle specific, real-world use cases. From f2ee2e6fbdb8b5666504f76d3cebd500b69e3647 Mon Sep 17 00:00:00 2001 From: Shay Cojocaru Date: 2020年10月14日 20:54:41 +0300 Subject: [PATCH 9/9] fix: remove excess 'the' (#22) --- chapters/ch02.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ch02.asciidoc b/chapters/ch02.asciidoc index 13051af..edbbe96 100644 --- a/chapters/ch02.asciidoc +++ b/chapters/ch02.asciidoc @@ -285,7 +285,7 @@ When a commonality can be found, abstractions involve less friction and help avo Another case where abstractions come in handy might be whenever we are dealing with quirks in cross-browser DOM manipulation. Having a function like `on(element, eventType, eventListener)` would be superior than testing whether `addEventListener` is supported and deciding which of the various event listening options is optimal for each case, every time, as it drastically reduces code duplication while also handling every case consistently, limiting complexity. -The above are clear-cut examples of cases when an abstraction greatly improves poor interfaces, but that's not always the end result. Abstractions can be a costly way of merging use cases when it's unclear whether those are naturally related in the first place. If we merge use cases too early, we might find that the the complexity we're tucking away in an abstraction is quite small -- and thus offset by the abstraction's own complexity. If we merge cases which weren't all that related to begin with, we'd be effectively increasing complexity and end up creating a tighter coupling than needed -- instead of lowering complexity like we set out to achieve, we end up obtaining the opposite result. +The above are clear-cut examples of cases when an abstraction greatly improves poor interfaces, but that's not always the end result. Abstractions can be a costly way of merging use cases when it's unclear whether those are naturally related in the first place. If we merge use cases too early, we might find that the complexity we're tucking away in an abstraction is quite small -- and thus offset by the abstraction's own complexity. If we merge cases which weren't all that related to begin with, we'd be effectively increasing complexity and end up creating a tighter coupling than needed -- instead of lowering complexity like we set out to achieve, we end up obtaining the opposite result. It is best to wait until a distinguishable pattern emerges and it becomes clear that introducing an abstraction would help keep complexity down. When such a pattern emerges, we can be confident that the use cases are indeed related, and we'll have better information about whether an abstraction would simplify our code.

AltStyle によって変換されたページ (->オリジナル) /