Skip to content

Commit da9daa1

Browse files
committed
address feedback from @RReverser
1 parent 489f3d8 commit da9daa1

File tree

2 files changed

+94
-31
lines changed

2 files changed

+94
-31
lines changed

chapters/ch05.asciidoc

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ Another way of doing composition, that doesn't rely on decorators, is to rely on
340340

341341
[source,javascript]
342342
----
343-
const emitters = new Map()
343+
const emitters = new WeakMap()
344344
345345
function onEvent(target, eventType, listener) {
346346
if (!emitters.has(target)) {
@@ -385,6 +385,8 @@ emitEvent(person, 'move', 23, 5)
385385
// <- 'Artemisa moved to [23, 5].'
386386
----
387387

388+
Note how we're using both `WeakMap` and `Map` here. Using a plain `Map` would prevent garbage collection from cleaning things up when `target` is only being referenced by `Map` entries, whereas `WeakMap` would allow garbage collection to take place. Given we usually want to attach events to objects, we can use `WeakMap` as a way to avoid creating strong references that might end up leading to memory leaks. On the other hand, it's okay to use a regular `Map` for the event listeners, given those are associated to an event type string.
389+
388390
Let's move onto deciding whether to use inheritance, decorators, or functional composition, where each pattern shines, and when to avoid them.
389391

390392
==== 5.2.3 Choosing between Composition and Inheritance
@@ -409,7 +411,7 @@ The revealing module pattern has become a staple in the world of JavaScript. The
409411

410412
Explicitly avoid exposing methods meant to be private, such as a hypothetical +_calculatePriceHistory+ method, which relies on the leading hyphen as a way of discouraging direct access and signaling that it should be regarded as private. Avoiding such methods prevents test code from accessing private methods directly, resulting in tests that make assertions solely regarding the interface and which can be later referenced as documentation on how to use the interface; prevents consumers from monkey-patching implementation details, leading to more transparent interfaces; and also often results in cleaner interfaces due to the fact that the interface is all there is, and there's no alternative ways of interacting with the module through direct use of its internals.
411413

412-
JavaScript modules are of a revealing nature by default, making it easy for us to follow the revealing pattern of not giving away access to implementation details. Any function, object, class, or variable we declare is private unless we explicitly decide to `export` it from the module.
414+
JavaScript modules are of a revealing nature by default, making it easy for us to follow the revealing pattern of not giving away access to implementation details. Functions, objects, classes, and any other bindings we declare are private unless we explicitly decide to `export` them from the module.
413415

414416
When we expose only a thin interface, our implementation can change largely without having an impact on how consumers use the module, nor on the tests that cover the module. As a mental exercise, always be on the lookout for aspects of an interface that should be turned into implementation details and extricated from the interface itself.
415417

@@ -419,61 +421,98 @@ Even when using JavaScript modules and following the revealing pattern strictly,
419421

420422
If we were to move our functional event emitter code snippet, with `onEvent` and `emitEvent`, into a JavaScript module, we'd notice that the `emitters` map is now a local global for that module, meaning all of the module's scope has access to `emitters`. This is what we'd want, because that way we can register event listeners in `onEvent` and fire them off in `emitEvent`. In most other situations, however, sharing persistent state across public interface methods is a recipe for unexpected bugs.
421423

422-
Suppose we have a `StringBuilder` module that can be used to join strings in a performant manner. Even if consumers were supposed to use it synchronously and flush state in one fell swoop, without giving way for a second consumer to taint the state and produce unexpected results, our module shouldn't rely on consumer behavior to provide consistent results. The following contrived implementation relies on local shared state, and would need consumers to use the module strictly as intended, making all calls to `append` and `stringify` in sequence.
424+
Suppose we have a `calculator` module that can be used to make basic calculations through a stream of operations. Even if consumers were supposed to use it synchronously and flush state in one fell swoop, without giving way for a second consumer to taint the state and produce unexpected results, our module shouldn't rely on consumer behavior to provide consistent results. The following contrived implementation relies on local shared state, and would need consumers to use the module strictly as intended, making all calls to `add`, `multiply`, and `calculate` in sequence.
423425

424426
[source,javascript]
425427
----
426-
const buffer = []
428+
const operations = []
429+
let state = 0
427430
428-
export function append(...text) {
429-
buffer.push(...text)
431+
export function add(value) {
432+
operations.push(() => {
433+
state += value
434+
})
435+
}
436+
437+
export function multiply(value) {
438+
operations.push(() => {
439+
state *= value
440+
})
430441
}
431442
432-
export function stringify() {
433-
return buffer.splice(0, buffer.length).join('')
443+
export function calculate() {
444+
operations.forEach(op => op())
445+
return state
434446
}
435447
----
436448

437-
Here's an example of how consuming the previous module could work. As soon as we try to asynchronously append text to the buffer, things start getting out of hand, with strings getting bits and pieces unrelated to what we expect.
449+
Here's an example of how consuming the previous module could work. As soon as we tried to asynchronously append text to the buffer, things would start getting out of hand, with the operations array getting bits and pieces of unrelated calculations, tainting our calculations.
438450

439451
[source,javascript]
440452
----
441-
const name = 'Sophie'
442-
append('hello')
443-
append(name)
444-
append(', it is nice to meet you!')
445-
stringify() // <- 'Hello Sophie, it is nice to meet you!'
453+
add(3)
454+
add(4)
455+
multiply(-2)
456+
calculate() // <- -14
446457
----
447458

448-
Blatantly, this contrived module is poorly designed, as its buffer should never be used to construct several unrelated strings. We should instead expose a factory function that returns an object from its own self-contained scope, where all relevant state is shut off from the outside world. The methods on this object are equivalent to the exported interface of a plain JavaScript module, but state mutations are contained to instances that consumers create.
459+
A slightly better approach would get rid of the `state` variable, and instead pass the state around operation handlers, so that each operation knows the current state, and applies any necessary changes to it. The `calculate` step would create a new initial state each time, and go from there.
449460

450461
[source,javascript]
451462
----
452-
function getStringBuilder() {
453-
const buffer = []
463+
const operations = []
464+
465+
export function add(value) {
466+
operations.push(state => state + value)
467+
}
468+
469+
export function multiply(value) {
470+
operations.push(state => state * value)
471+
}
472+
473+
export function calculate() {
474+
return operations.reduce((result, op) =>
475+
op(result)
476+
, 0)
477+
}
478+
----
479+
480+
This approach presents problems too, however. Even though the `state` will always be reset to `0`, we're treating unrelated operations as if they were all part of a whole, which is still wrong.
481+
482+
Blatantly, this contrived module is poorly designed, as its operations buffer should never be used to drive several unrelated calculations. We should instead expose a factory function that returns an object from its own self-contained scope, where all relevant state is shut off from the outside world. The methods on this object are equivalent to the exported interface of a plain JavaScript module, but state mutations are contained to instances that consumers create.
483+
484+
[source,javascript]
485+
----
486+
export function getCalculator() {
487+
const operations = []
488+
489+
function add(value) {
490+
operations.push(state => state + value)
491+
}
454492
455-
function append(...text) {
456-
buffer.push(...text)
493+
function multiply(value) {
494+
operations.push(state => state * value)
457495
}
458496
459-
function stringify() {
460-
return buffer.splice(0, buffer.length).join('')
497+
function calculate() {
498+
return operations.reduce((result, op) =>
499+
op(result)
500+
, 0)
461501
}
462502
463-
return { append, stringify }
503+
return { add, multiply, calculate }
464504
}
465505
----
466506

467-
Using the string builder like this is just as straightforward, except that now we can do things asynchronously and even if other consumers are also using string builders of their own, each user will have their own state, preventing corrupt data.
507+
Using the calculator like this is just as straightforward, except that now we can do things asynchronously and even if other consumers are also using string builders of their own, each user will have their own state, preventing data corruption.
468508

469509
[source,javascript]
470510
----
471-
const { append, stringify } = getStringBuilder()
472-
const name = 'Sophie'
473-
append('hello')
474-
append(name)
475-
append(', it is nice to meet you!')
476-
stringify() // <- 'Hello Sophie, it is nice to meet you!'
511+
const { append, stringify } = getCalculator()
512+
add(3)
513+
add(4)
514+
multiply(-2)
515+
calculate() // <- -14
477516
----
478517

479518
As we just showed, even when using modern language constructs and JavaScript modules, it's not too hard to create complications through shared state. Thus, we should always strive to contain mutable state as close to its consumers as possible.

chapters/ch06.asciidoc

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,33 @@ A secret service also takes care of encryption, secure storage, secret rotation
124124

125125
==== 6.2 Explicit Dependency Management
126126

127-
It's not practical to include dependencies in our repositories, given these are often in the hundreds of megabytes and often include environment-dependant and operating system dependant files. During development, we want to make sure we get non-breaking upgrades to our dependencies, which can help us resolve bugs and tighten our grip around security vulnerabilities. For deployments, we want reproducible builds, where installing our dependencies yields the same results every time. The solution is often to include a dependency manifest, indicating what exact versions of the libraries in our dependency tree we want to be installing. This can be accomplished with npm (starting with version 5) and its `package-lock.json` manifest, as well as through the Yarn package manager and its `yarn.lock` manifest, both of which we should be publishing to our versioned repository.
127+
It's not practical to include dependencies in our repositories, given these are often in the hundreds of megabytes and often include environment-dependant and operating system dependant files. During development, we want to make sure we get non-breaking upgrades to our dependencies, which can help us resolve bugs and tighten our grip around security vulnerabilities. For deployments, we want reproducible builds, where installing our dependencies yields the same results every time.
128128

129-
Every dependency in our application should be explicitly declared in our manifest, relying on globally installed packages or global variables as little as possible. Implicit dependencies involve additional steps across environments, where developers and deployment flows alike must take action to ensure these extra dependencies are installed, beyond what a simple `npm install` step could achieve.
129+
The solution is often to include a dependency manifest, indicating what exact versions of the libraries in our dependency tree we want to be installing. This can be accomplished with npm (starting with version 5) and its `package-lock.json` manifest, as well as through Facebook's Yarn package manager and its `yarn.lock` manifest, both of which we should be publishing to our versioned repository.
130+
131+
Using these manifests across environments ensures we get reproducible installs of our dependencies, meaning everyone working with the codebase -- as well as hosted environments -- deals with the same package versions, both at the top level (direct dependencies) and regardless the nesting depth (dependencies of dependencies -- of dependencies).
132+
133+
Every dependency in our application should be explicitly declared in our manifest, relying on globally installed packages or global variables as little as possible. Implicit dependencies involve additional steps across environments, where developers and deployment flows alike must take action to ensure these extra dependencies are installed, beyond what a simple `npm install` step could achieve. Here's an example of how a `package-lock.json` file might look:
134+
135+
```
136+
{
137+
"name": "A",
138+
"version": "0.1.0",
139+
// metadata…
140+
"dependencies": {
141+
"B": {
142+
"version": "0.0.1",
143+
"resolved": "https://registry.npmjs.org/B/-/B-0.0.1.tgz",
144+
"integrity": "sha512-DeAdb33F+"
145+
"dependencies": {
146+
"C": {
147+
"version": "git://github.com/org/C.git#5c380ae319fc4efe9e7f2d9c78b0faa588fd99b4"
148+
}
149+
}
150+
}
151+
}
152+
}
153+
```
130154

131155
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.
132156

0 commit comments

Comments
 (0)