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
+10-5Lines changed: 10 additions & 5 deletions
Original file line number
Diff line number
Diff line change
@@ -87,7 +87,8 @@ The following example shows an email component that's only concerned with config
87
87
----
88
88
import mailApi from 'mail-api'
89
89
import { mailApiSecret } from './secrets'
90
-
export default function send (options, done) {
90
+
91
+
export default function send(options, done) {
91
92
const { to, subject, html } = options
92
93
const client = mailApi({ mailApiSecret })
93
94
client.send({
@@ -103,7 +104,7 @@ It wouldn't be hard to create a drop-in replacement by developing a module which
103
104
104
105
[source,javascript]
105
106
----
106
-
export default function send(options, done) {
107
+
export default function send(options, done) {
107
108
const { to, subject, html } = options
108
109
console.log(`
109
110
Sending email.
@@ -120,12 +121,14 @@ By the same token, a templating component could be developed orthogonally, with
120
121
[source,javascript]
121
122
----
122
123
import insane from 'insane'
123
-
function sanitize (template, ...expressions) {
124
+
125
+
function sanitize(template, ...expressions) {
124
126
return template.reduce((result, part, i) =>
125
127
result + insane(expressions[i - 1]) + part
126
128
)
127
129
}
128
-
export default function compile (model) {
130
+
131
+
export default function compile(model) {
129
132
const { title, body, tags } = model
130
133
const html = sanitize`
131
134
<h1>${ title }</h1>
@@ -142,7 +145,9 @@ export default function compile (model) {
142
145
}
143
146
----
144
147
145
-
Changing the API slightly shouldn't be an issue, as long as the API remains consistent across the components we want to make interchangeable. A different implementation could take a template identifier, in addition to the +model+ object, so that the template itself is also decoupled from the +compile+ function.
148
+
Slightly modifying the API shouldn't be an issue, as long as it remains consistent across the components we want to make interchangeable. For instance, a different implementation could take a template identifier, in addition to the +model+ object, so that the template itself is also decoupled from the +compile+ function.
149
+
150
+
When we keep the API consistent across implementationsfootnoteref:[impl-examples, For example, one implementation might merely compile an HTML email using inline templates, another might use HTML template files, another could rely on a third party service, and yet another could compile emails as plain-text instead.], using the same signature across every module, it's easy to swap out implementations depending on context such as the execution environment (development vs. staging vs. production) or any other dynamic context that we need to rely upon.
146
151
147
152
As we mentioned earlier, a third module could plumb together different components which handle separate concerns, such as templating and email sending. The following example leverages the logging email provider and the static templating function to join both concerns together. Interestingly, this module doesn't break SRP either, as its only concern is to plumb other modules together.
Copy file name to clipboardExpand all lines: chapters/ch05.asciidoc
+5-5Lines changed: 5 additions & 5 deletions
Original file line number
Diff line number
Diff line change
@@ -333,11 +333,11 @@ The introduction of a `class` keyword, paired with the React framework originall
333
333
334
334
Inheritance is also useful when there's an abstract interface to implement and methods to override, particularly when the objects being represented can be mapped to the real world. In practical terms and in the case of JavaScript, inheritance works great when the prototype being extended offers a good description for the parent prototype: a `Car` is a `Vehicle` but a car is not a `SteeringWheel`: the wheel is just one aspect of the car.
335
335
336
-
==== 5.2.2 The Perks of Composition: Aspects and Decorators
336
+
==== 5.2.2 The Perks of Composition: Aspects and Extensions
337
337
338
338
With inheritance we can add layers of complexity to an object. These layers are meant to be ordered: we start with the least specific foundational bits of the object and build our way up to the most specific aspects of it. When we write code based on inheritance chains, complexity is spread across the different classes, but lies mostly at the foundational layers which offer a terse API while hiding this complexity away. Composition is an alternative to inheritance. Rather than building objects by vertically stacking functionality, composition relies on stringing together orthogonal aspects of functionality. In this sense, orthogonality means that the bits of functionality we compose together complements each other, but doesn't alter one another's behavior.
339
339
340
-
One way to compose functionality is additive: we could write decorators, which augment existing objects with new functionality. In the following code snippet we have a `makeEmitter` function which adds flexible event handling functionality to any target object, providing them with an `.on` method, where we can add event listeners to the target object; and an `.emit` method, where the consumer can indicate a type of event and any number of parameters to be passed to event listeners.
340
+
One way to compose functionality is additive: we could write extension functions, which augment existing objects with new functionality. In the following code snippet we have a `makeEmitter` function which adds flexible event handling functionality to any target object, providing them with an `.on` method, where we can add event listeners to the target object; and an `.emit` method, where the consumer can indicate a type of event and any number of parameters to be passed to event listeners.
341
341
342
342
[source,javascript]
343
343
----
@@ -380,7 +380,7 @@ person.emit('move', 23, 5)
380
380
381
381
This approach is versatile, helping us add event emission functionality to any object without the need for adding an `EventEmitter` class somewhere in the prototype chain of the object. This is useful in cases where you don't own the base class, when the targets aren't class-based, or when the functionality to be added isn't meant to be part of every instance of a class: there are persons who emit events and persons that are quiet and don't need this functionality.
382
382
383
-
Another way of doing composition, that doesn't rely on decorators, is to rely on functional aspects instead, without mutating your target object. In the following snippet we do just that: we have an `emitters` map where we store target objects and map them to the event listeners they have, an `onEvent` function that associates event listeners to target objects, and an `emitEvent` function that fires all event listeners of a given type for a target object, passing the provided parameters. All of this is accomplished in such a way that there's no need to modify the `person` object in order to have event handling capabilities associated with the object.
383
+
Another way of doing composition, that doesn't rely on extension functions, is to rely on functional aspects instead, without mutating your target object. In the following snippet we do just that: we have an `emitters` map where we store target objects and map them to the event listeners they have, an `onEvent` function that associates event listeners to target objects, and an `emitEvent` function that fires all event listeners of a given type for a target object, passing the provided parameters. All of this is accomplished in such a way that there's no need to modify the `person` object in order to have event handling capabilities associated with the object.
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` allows garbage collection to take place on the objects that make up its keys. 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 causing 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.
433
433
434
-
Let's move onto deciding whether to use inheritance, decorators, or functional composition, where each pattern shines, and when to avoid them.
434
+
Let's move onto deciding whether to use inheritance, extension functions, or functional composition, where each pattern shines, and when to avoid them.
435
435
436
436
==== 5.2.3 Choosing between Composition and Inheritance
437
437
@@ -441,7 +441,7 @@ Decoration and functional composition are friendlier patterns because they aren'
441
441
442
442
The functional approach is a bit more cumbersome to implement than simply mutating objects or adding base classes, but it offers the most flexibility. By avoiding changes to the underlying target, we keep objects easy to serialize into JSON, unencumbered by a growing collection of methods, and thus more readily compatible across our codebase.
443
443
444
-
Furthermore, using base classes makes it a bit hard to reuse the logic at varying insertion points in our prototype chains. Using decorators, likewise, makes it challenging to add similar methods that support slightly different use cases. Using a functional approach leads to less coupling in this regard, but it could also complicate the underlying implementation of the makeup for objects, making it hard to decypher how their functionality ties in together, tainting our fundamental understanding of how code flows and making debugging sessions longer than need be.
444
+
Furthermore, using base classes makes it a bit hard to reuse the logic at varying insertion points in our prototype chains. Using extension functions, likewise, makes it challenging to add similar methods that support slightly different use cases. Using a functional approach leads to less coupling in this regard, but it could also complicate the underlying implementation of the makeup for objects, making it hard to decypher how their functionality ties in together, tainting our fundamental understanding of how code flows and making debugging sessions longer than need be.
445
445
446
446
As with most things programming, your codebase benefits from a semblance of consistency. Even if you use all three patterns, -- and others -- a codebase that uses half a dozen patterns in equal amounts is harder to understand, work with, and build on, than an equivalent codebase that instead uses one pattern for the vast majority of its code while using other patterns in smaller ways when warranted. Picking the right pattern for each situation and striving for consistency might seem at odds with each other, but this is again a balancing act. The trade-off is between consistency in the grand scale of our codebase versus simplicity in the local piece of code we're working on. The question to ask is then: are we obtaining enough of a simplicity gain that it warrants the sacrifice of some consistency?
0 commit comments