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
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
+
388
390
Let's move onto deciding whether to use inheritance, decorators, or functional composition, where each pattern shines, and when to avoid them.
389
391
390
392
==== 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
409
411
410
412
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.
411
413
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.
413
415
414
416
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.
415
417
@@ -419,61 +421,98 @@ Even when using JavaScript modules and following the revealing pattern strictly,
419
421
420
422
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.
421
423
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.
423
425
424
426
[source,javascript]
425
427
----
426
-
const buffer = []
428
+
const operations = []
429
+
let state = 0
427
430
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
+
})
430
441
}
431
442
432
-
export function stringify() {
433
-
return buffer.splice(0, buffer.length).join('')
443
+
export function calculate() {
444
+
operations.forEach(op => op())
445
+
return state
434
446
}
435
447
----
436
448
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.
438
450
439
451
[source,javascript]
440
452
----
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
446
457
----
447
458
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.
449
460
450
461
[source,javascript]
451
462
----
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
+
}
454
492
455
-
function append(...text) {
456
-
buffer.push(...text)
493
+
function multiply(value) {
494
+
operations.push(state => state * value)
457
495
}
458
496
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)
461
501
}
462
502
463
-
return { append, stringify }
503
+
return { add, multiply, calculate }
464
504
}
465
505
----
466
506
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.
468
508
469
509
[source,javascript]
470
510
----
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
477
516
----
478
517
479
518
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.
Copy file name to clipboardExpand all lines: chapters/ch06.asciidoc
+26-2Lines changed: 26 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -124,9 +124,33 @@ A secret service also takes care of encryption, secure storage, secret rotation
124
124
125
125
==== 6.2 Explicit Dependency Management
126
126
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.
128
128
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:
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.
0 commit comments