Skip to content

Commit b88f211

Browse files
committed
Inheritance, functional composition, decorators.
1 parent 48bfecd commit b88f211

File tree

1 file changed

+102
-7
lines changed

1 file changed

+102
-7
lines changed

chapters/ch05.asciidoc

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -224,28 +224,123 @@ We can apply our guidelines of what constitutes clear code to asynchronous code
224224

225225
Let's explore how we can improve our application designs beyond what JavaScript offers purely at the language level. In this section we'll discuss two different approaches to growing parts of a codebase: inheritance, where we scale vertically by stacking pieces of code on top of each other so that we can leverage existing features while customizing others and adding our own; and composition, where we scale our application horizontally by adding related or unrelated pieces of code at the same level of abstraction while keeping complexity to a minimum.
226226

227-
==== 5.2.1 The Perks of Inheritance
227+
==== 5.2.1 Inheritance through Classes
228228

229-
Up until ES6 introduced first-class syntax for prototypal inheritance to JavaScript, it wasn't a widely used feature in user-land. However, with the introduction of a `class` keyword, paired with the React framework hailing classes as the go-to way of declaring stateful components, classes have shed some light on a pattern that was previously unpopular (at least when it comes to JavaScript).
229+
Up until ES6 introduced first-class syntax for prototypal inheritance to JavaScript, prototypes weren't a widely used feature in user-land. Instead, libraries offered helper methods that made inheritance simpler, using prototypal inheritance under the hood, but hiding the implementation details from their consumers. Even though ES6 classes look a lot like classes in other languages, they're syntactic sugar using prototypes under the hood, making them compatible with older techniques and libraries.
230230

231-
Inheritance is 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.
231+
The introduction of a `class` keyword, paired with the React framework hailing classes as the go-to way of declaring stateful components, classes have helped spark some love for a pattern that was previously quite unpopular when it comes to JavaScript. In the case of React, the base `Component` class offers lightweight state management methods, while leaving the rendering and lifecycle up to the consumer classes extending `Component`. When necessary, the consumer can also decide to implement methods such as `componentDidMount`, which allows for event binding after a component tree is mounted; `componentDidCatch`, which can be used to trap unhandled exceptions that arise during the component lifecycle; among a variety of other soft interface methods. There's no mention of these optional lifecycle hooks anywhere in the base `Component` class, which are instead confined to the rendering mechanisms of React. In this sense, we note that the `Component` class stays focused on state management, while everything else is offered up by the consumer.
232232

233-
In the case of React, the `Component` class dictates the standard behavior and lifecycle of components, while leaving the rendering up to the inheriting class. When necessary, the consumer can also decide to implement methods such as `componentDidMount`, which allows for event binding after a component tree is mounted; `componentDidCatch`, which can be used to trap unhandled exceptions anywhere in the component lifecycle; among a variety of other virtual methods.
233+
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.
234234

235+
==== 5.2.2 The Perks of Composition: Aspects and Decorators
235236

237+
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.
236238

239+
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.
237240

241+
[source,javascript]
242+
----
243+
function makeEmitter(target) {
244+
const listeners = []
245+
246+
target.on = (eventType, listener) => {
247+
if (!(eventType in listeners)) {
248+
listeners[eventType] = []
249+
}
250+
251+
listeners[eventType].push(listener)
252+
}
253+
254+
target.emit = (eventType, ...params) => {
255+
if (!(eventType in listeners)) {
256+
return
257+
}
258+
259+
listeners[eventType].forEach(listener => {
260+
listener(...params)
261+
})
262+
}
263+
264+
return target
265+
}
266+
267+
const person = makeEmitter({
268+
name: 'Artemisa',
269+
age: 27
270+
})
271+
272+
person.on('move', (x, y) => {
273+
console.log(`${ person.name } moved to [${ x }, ${ y }].`)
274+
})
275+
276+
person.emit('move', 23, 5)
277+
// <- 'Artemisa moved to [23, 5].'
278+
----
279+
280+
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.
281+
282+
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.
283+
284+
[source,javascript]
285+
----
286+
const emitters = new Map()
287+
288+
function onEvent(target, eventType, listener) {
289+
if (!emitters.has(target)) {
290+
emitters.set(target, {})
291+
}
238292
293+
const listeners = emitters.get(target)
239294
295+
if (!(eventType in listeners)) {
296+
listeners[eventType] = []
297+
}
240298
299+
listeners[eventType].push(listener)
300+
}
241301
242-
==== 5.2.2 Composition: Aspects and Decorators
302+
function emitEvent(target, eventType, ...params) {
303+
if (!emitters.has(target)) {
304+
return
305+
}
243306
244-
how we can decorate components either via composition or decorators to add features to them, and reuse that logic for other kinds of components..
307+
const listeners = emitters.get(target)
308+
309+
if (!(eventType in listeners)) {
310+
return
311+
}
312+
313+
listeners[eventType].forEach(listener => {
314+
listener(...params)
315+
})
316+
}
317+
318+
const person = {
319+
name: 'Artemisa',
320+
age: 27
321+
}
322+
323+
onEvent(person, 'move', (x, y) => {
324+
console.log(`${ person.name } moved to [${ x }, ${ y }].`)
325+
})
326+
327+
emitEvent(person, 'move', 23, 5)
328+
// <- 'Artemisa moved to [23, 5].'
329+
----
330+
331+
Let's move onto deciding whether to use inheritance, decorators, or functional composition, where each pattern shines, and when to avoid them.
245332

246333
==== 5.2.3 Choosing between Composition and Inheritance
247334

248-
in real apps you'll seldom have to use inheritance except when connecting to specific frameworks you depend on, or for specific patterns, everywhere else probably use composition [ellaborate on that]
335+
In the real world, you'll seldom have to use inheritance except when connecting to specific frameworks you depend on, to apply specific patterns such as extending native JavaScript arrays, or when performance is of the utmost necessity. When it comes to performance as a reason for using prototypes, we should highlight the need to test our assumptions and measure different approaches before jumping all in into a pattern that might not be ideal to work with, for the sake of a performance gain we might not observe.
336+
337+
Decoration and functional composition are friendlier patterns because they aren't as restrictive. Once you inherit from something, you can't later choose to inherit from something else, unless you keep adding inheritance layers to your prototype chain. This becomes a problem when several classes inherit from a base class but they then need to branch out while still sharing different portions of functionality. In these cases and many others, using composition is going to let us pick and choose the functionality we need without sacrificing our flexibility.
338+
339+
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.
340+
341+
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.
342+
343+
As with most things programming, your codebase will benefit 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 will be harder to understand, work with, and build on, than an equivalnet 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?
249344

250345
=== 5.3 Code Patterns
251346

0 commit comments

Comments
 (0)