In this chapter, we will explore some of the most popular structural design patterns and discover how they apply to Node.js. Structural design patterns are focused on providing ways to realize relationships between entities.
In particular, in this chapter, we will examine the following patterns:
Throughout the chapter, we will also explore some interesting concepts such as reactive programming (RP), and we will also spend some time playing with LevelDB, a database technology that is commonly adopted in the Node.js ecosystem.
By the end of this chapter, you will be familiar with several scenarios in which structural design patterns can be useful and you will be able to implement them effectively in your Node.js applications.
A proxy is an object that controls access to another object, called the subject. The proxy and the subject have an identical interface, and this allows us to swap one for the other transparently; in fact, the alternative name for this pattern is surrogate.
A proxy intercepts all or some of the operations that are meant to be executed on the subject, augmenting or complementing their behavior. Figure 8.1 shows a schematic representation of this pattern:
Figure 8.1: Proxy pattern schematic
Figure 8.1 shows us how the proxy and the subject have the same interface, and how this is transparent to the client, who can use one or the other interchangeably. The proxy forwards each operation to the subject, enhancing its behavior with additional preprocessing or postprocessing.
It's important to observe that we are not talking about proxying between classes; the Proxy pattern involves wrapping an actual instance of the subject, thus preserving its internal state.
A proxy can be useful in several circumstances, for example:
There are more Proxy pattern applications, but these should give us an idea of its purpose.
When proxying an object, we can decide to intercept all of its methods or only some of them, while delegating the rest directly to the subject. There are several ways in which this can be achieved, and in this section, we will present some of them.
We will be working on a simple example, a StackCalculator
class that looks like this:
class StackCalculator {
constructor () {
this.stack = []
}
putValue (value) {
this.stack.push(value)
}
getValue () {
return this.stack.pop()
}
peekValue () {
return this.stack[this.stack.length - 1]
}
clear () {
this.stack = []
}
divide () {
const divisor = this.getValue()
const dividend = this.getValue()
const result = dividend / divisor
this.putValue(result)
return result
}
multiply () {
const multiplicand = this.getValue()
const multiplier = this.getValue()
const result = multiplier * multiplicand
this.putValue(result)
return result
}
}
This class implements a simplified version of a stack calculator. The idea of this calculator is to keep all operands (values) in a stack. When you perform an operation, for example a multiplication, the multiplicand and the multiplier are extracted from the stack and the result of the multiplication is pushed back into the stack. This is not too different from how the calculator application on your mobile phone is actually implemented.
Here's an example of how we might use StackCalculator
to perform some multiplications and divisions:
const calculator = new StackCalculator()
calculator.putValue(3)
calculator.putValue(2)
console.log(calculator.multiply()) // 3*2 = 6
calculator.putValue(2)
console.log(calculator.multiply()) // 6*2 = 12
There are also some utility methods such as peekValue()
, which allows us to peek the value at the top of the stack (the last value inserted or the result of the last operation), and clear()
, which allows us to reset the stack.
Fun fact: In JavaScript, when you perform a division by 0, you get back a mysterious value called Infinity
. In many other programming languages dividing by 0 is an illegal operation that results in the program panicking or throwing a runtime exception.
Our task in the next few sections will be to leverage the Proxy pattern to enhance a StackCalculator
instance by providing a more conservative behavior for division by 0: rather than returning Infinity
, we will throw an explicit error.
Composition is a technique whereby an object is combined with another object for the purpose of extending or using its functionality. In the specific case of the Proxy pattern, a new object with the same interface as the subject is created, and a reference to the subject is stored internally in the proxy in the form of an instance variable or a closure variable. The subject can be injected from the client at creation time or created by the proxy itself.
The following example implements a safe calculator using object composition:
class SafeCalculator {
constructor (calculator) {
this.calculator = calculator
}
// proxied method
divide () {
// additional validation logic
const divisor = this.calculator.peekValue()
if (divisor === 0) {
throw Error('Division by 0')
}
// if valid delegates to the subject
return this.calculator.divide()
}
// delegated methods
putValue (value) {
return this.calculator.putValue(value)
}
getValue () {
return this.calculator.getValue()
}
peekValue () {
return this.calculator.peekValue()
}
clear () {
return this.calculator.clear()
}
multiply () {
return this.calculator.multiply()
}
}
const calculator = new StackCalculator()
const safeCalculator = new SafeCalculator(calculator)
calculator.putValue(3)
calculator.putValue(2)
console.log(calculator.multiply()) // 3*2 = 6
safeCalculator.putValue(2)
console.log(safeCalculator.multiply()) // 6*2 = 12
calculator.putValue(0)
console.log(calculator.divide()) // 12/0 = Infinity
safeCalculator.clear()
safeCalculator.putValue(4)
safeCalculator.putValue(0)
console.log(safeCalculator.divide()) // 4/0 -> Error
The safeCalculator
object is a proxy for the original calculator
instance. By invoking multiply()
on safeCalculator
, we will end up calling the same method on calculator
. The same goes for divide()
, but in this case we can see that, if we try to divide by zero, we will get different outcomes depending on whether we perform the division on the subject or on the proxy.
To implement this proxy using composition, we had to intercept the methods that we were interested in manipulating (divide()
), while simply delegating the rest of them to the subject (putValue()
, getValue()
, peekValue()
, clear()
, and multiply()
).
Note that the calculator state (the values in the stack) is still maintained by the calculator
instance; safeCalculator
will only invoke methods on calculator
to read or mutate the state as needed.
An alternative implementation of the proxy presented in the preceding code fragment might just use an object literal and a factory function:
function createSafeCalculator (calculator) {
return {
// proxied method
divide () {
// additional validation logic
const divisor = calculator.peekValue()
if (divisor === 0) {
throw Error('Division by 0')
}
// if valid delegates to the subject
return calculator.divide()
},
// delegated methods
putValue (value) {
return calculator.putValue(value)
},
getValue () {
return calculator.getValue()
},
peekValue () {
return calculator.peekValue()
},
clear () {
return calculator.clear()
},
multiply () {
return calculator.multiply()
}
}
}
const calculator = new StackCalculator()
const safeCalculator = createSafeCalculator(calculator)
// ...
This implementation is simpler and more concise than the class-based one, but, once again, it forces us to delegate all the methods to the subject explicitly.
Having to delegate many methods for complex classes can be very tedious and might make it harder to implement these techniques. One way to create a proxy that delegates most of its methods is to use a library that generates all the methods for us, such as delegates
(nodejsdp.link/delegates). A more modern and native alternative is to use the Proxy
object, which we will discuss later in this chapter.
Object augmentation (or monkey patching) is probably the simplest and most common way of proxying just a few methods of an object. It involves modifying the subject directly by replacing a method with its proxied implementation.
In the context of our calculator example, this could be done as follows:
function patchToSafeCalculator (calculator) {
const divideOrig = calculator.divide
calculator.divide = () => {
// additional validation logic
const divisor = calculator.peekValue()
if (divisor === 0) {
throw Error('Division by 0')
}
// if valid delegates to the subject
return divideOrig.apply(calculator)
}
return calculator
}
const calculator = new StackCalculator()
const safeCalculator = patchToSafeCalculator(calculator)
// ...
This technique is definitely convenient when we need to proxy only one or a few methods. Did you notice that we didn't have to reimplement the multiply()
method and all the other delegated methods here?
Unfortunately, simplicity comes at the cost of having to mutate the subject
object directly, which can be dangerous.
Mutations should be avoided at all costs when the subject is shared with other parts of the codebase. In fact, "monkey patching" the subject might create undesirable side effects that affect other components of our application. Use this technique only when the subject exists in a controlled context or in a private scope. If you want to appreciate why "monkey patching" is a dangerous practice, you could try to invoke a division by zero in the original calculator
instance. If you do so, you will see that the original instance will now throw an error rather than returning Infinity
. The original behavior has been altered, and this might have unexpected effects on other parts of the application.
In the next section, we will explore the built-in Proxy
object, which is a powerful alternative for implementing the Proxy pattern and more.
The ES2015 specification introduced a native way to create powerful proxy objects.
We are talking about the ES2015 Proxy
object, which consists of a Proxy
constructor that accepts a target
and a handler
as arguments:
const proxy = new Proxy(target, handler)
Here, target
represents the object on which the proxy is applied (the subject for our canonical definition), while handler
is a special object that defines the behavior of the proxy.
The handler
object contains a series of optional methods with predefined names called trap methods (for example, apply
, get
, set
, and has
) that are automatically called when the corresponding operations are performed on the proxy instance.
To better understand how this API works, let's see how we can use the Proxy
object to implement our safe calculator proxy:
const safeCalculatorHandler = {
get: (target, property) => {
if (property === 'divide') {
// proxied method
return function () {
// additional validation logic
const divisor = target.peekValue()
if (divisor === 0) {
throw Error('Division by 0')
}
// if valid delegates to the subject
return target.divide()
}
}
// delegated methods and properties
return target[property]
}
}
const calculator = new StackCalculator()
const safeCalculator = new Proxy(
calculator,
safeCalculatorHandler
)
// ...
In this implementation of the safe calculator proxy using the Proxy
object, we adopted the get
trap to intercept access to properties and methods of the original object, including calls to the divide()
method. When access to divide()
is intercepted, the proxy returns a modified version of the function that implements the additional logic to check for possible divisions by zero. Note that we can simply return all other methods and properties unchanged by using target[property]
.
Finally, it is important to mention that the Proxy
object inherits the prototype of the subject, therefore running safeCalculator instanceof StackCalculator
will return true
.
With this example, it should be clear that the Proxy
object allows us to avoid mutating the subject while giving us an easy way to proxy only the bits that we need to enhance, without having to explicitly delegate all the other properties and methods.
The Proxy
object is a feature deeply integrated into the JavaScript language itself, which enables developers to intercept and customize many operations that can be performed on objects. This characteristic opens up new and interesting scenarios that were not easily achievable before, such as meta-programming, operator overloading, and object virtualization.
Let's see another example to clarify this concept:
const evenNumbers = new Proxy([], {
get: (target, index) => index * 2,
has: (target, number) => number % 2 === 0
})
console.log(2 in evenNumbers) // true
console.log(5 in evenNumbers) // false
console.log(evenNumbers[7]) // 14
In this example, we are creating a virtual array that contains all even numbers. It can be used as a regular array, which means we can access items in the array with the regular array syntax (for example, evenNumbers[7]
), or check the existence of an element in the array with the in
operator (for example, 2 in evenNumbers
). The array is considered virtual because we never store data in it.
It is very important to note that, while the previous code snippet is a very interesting example that aims to showcase some of the advanced capabilities of the Proxy
object, it is not implementing the Proxy pattern. This example allows us to see that, even though the Proxy
object is commonly used to implement the Proxy pattern (hence the name), it can also be used to implement other patterns and use cases. As an example, we will see later in this chapter how to use the Proxy
object—to implement the Decorator pattern.
Looking at the implementation, this proxy uses an empty array as the target and then defines the get
and has
traps in the handler:
get
trap intercepts access to the array elements, returning the even number for the given indexhas
trap instead intercepts the usage of the in
operator and checks whether the given number is even or notThe Proxy
object supports several other interesting traps such as set
, delete
, and construct
, and allows us to create proxies that can be revoked on demand, disabling all the traps and restoring the original behavior of the target
object.
Analyzing all these features goes beyond the scope of this chapter; what is important here is understanding that the Proxy
object provides a powerful foundation for implementing the Proxy design pattern.
If you are curious to discover all the capabilities and trap methods offered by the Proxy object, you can read more in the related MDN article at nodejsdp.link/mdn-proxy. Another good source is this detailed article from Google at nodejsdp.link/intro-proxy.
While the Proxy
object is a powerful functionality of the JavaScript language, it suffers from a very important limitation: the Proxy
object cannot be fully transpiled or polyfilled. This is because some of the Proxy
object traps can be implemented only at the runtime level and cannot be simply rewritten in plain JavaScript. This is something to be aware of if you are working with old browsers or old versions of Node.js that don't support the Proxy
object directly.
Transpilation: Short for transcompilation. It indicates the action of compiling source code by translating it from one source programming language to another. In the case of JavaScript, this technique is used to convert a program using new capabilities of the language into an equivalent program that can also run on older runtimes that do not support these new capabilities.
Polyfill: Code that provides an implementation for a standard API in plain JavaScript and that can be imported in environments where this API is not available (generally older browsers or runtimes). core-js
(nodejsdp.link/corejs) is one of the most complete polyfill libraries for JavaScript.
Composition can be considered a simple and safe way of creating a proxy because it leaves the subject untouched without mutating its original behavior. Its only drawback is that we have to manually delegate all the methods, even if we want to proxy only one of them. Also, we might have to delegate access to the properties of the subject.
Object properties can be delegated using Object.defineProperty()
. Find out more at nodejsdp.link/define-prop.
Object augmentation, on the other hand, modifies the subject, which might not always be ideal, but it does not suffer from the various inconveniences related to delegation. For this reason, between these two approaches, object augmentation is generally the preferred technique in all those circumstances in which modifying the subject is an option.
However, there is at least one situation where composition is almost necessary; this is when we want to control the initialization of the subject, for example, to create it only when needed (lazy initialization).
Finally, the Proxy
object is the go-to approach if you need to intercept function calls or have different types of access to object attributes, even dynamic ones. The Proxy
object provides an advanced level of access control that is simply not available with the other techniques. For example, the Proxy
object allows us to intercept the deletion of a key in an object and to perform property existence checks.
Once again, it's worth highlighting that the Proxy
object does not mutate the subject, so it can be safely used in contexts where the subject is shared between different components of the application. We also saw that with the Proxy
object, we can easily perform delegation of all the methods and attributes that we want to leave unchanged.
In the next section, we present a more realistic example leveraging the Proxy pattern and use it to compare the different techniques we have discussed so far for implementing this pattern.
To see the Proxy pattern applied to a real example, we will now build an object that acts as a proxy to a Writable stream, which intercepts all the calls to the write()
method and logs a message every time this happens. We will use the Proxy
object to implement our proxy. Let's write our code in a file called logging-writable.js
:
export function createLoggingWritable (writable) {
return new Proxy(writable, { // (1)
get (target, propKey, receiver) { // (2)
if (propKey === 'write') { // (3)
return function (...args) { // (4)
const [chunk] = args
console.log('Writing', chunk)
return writable.write(...args)
}
}
return target[propKey] // (5)
}
})
}
In the preceding code, we created a factory that returns a proxied version of the writable
object passed as an argument. Let's see what the main points of the implementation are:
writable
object using the ES2015 Proxy
constructor.get
trap to intercept access to the object properties.write
method. If that is the case, we return a function to proxy the original behavior.chunk
from the list of arguments passed to the original function, we log the content of the chunk, and finally, we invoke the original method with the given list of arguments.We can now use this newly created function and test our proxy implementation:
import { createWriteStream } from 'fs'
import { createLoggingWritable } from './logging-writable.js'
const writable = createWriteStream('test.txt')
const writableProxy = createLoggingWritable(writable)
writableProxy.write('First chunk')
writableProxy.write('Second chunk')
writable.write('This is not logged')
writableProxy.end()
The proxy did not change the original interface of the stream or its external behavior, but if we run the preceding code, we will now see that every chunk that is written into the writableProxy
stream is transparently logged to the console.
The Change Observer pattern is a design pattern in which an object (the subject) notifies one or more observers of any state changes, so that they can "react" to changes as soon as they happen.
Although very similar, the Change Observer pattern should not be confused with the Observer pattern discussed in Chapter 3, Callbacks and Events. The Change Observer pattern focuses on allowing the detection of property changes, while the Observer pattern is a more generic pattern that adopts an event emitter to propagate information about events happening in the system.
Proxies turn out to be quite an effective tool to create observable objects. Let's see a possible implementation with create-observable.js
:
export function createObservable (target, observer) {
const observable = new Proxy(target, {
set (obj, prop, value) {
if (value !== obj[prop]) {
const prev = obj[prop]
obj[prop] = value
observer({ prop, prev, curr: value })
}
return true
}
})
return observable
}
In the previous code, createObservable()
accepts a target
object (the object to observe for changes) and an observer
(a function to invoke every time a change is detected).
Here, we create the observable
instance through an ES2015 Proxy. The proxy implements the set
trap, which is triggered every time a property is set. The implementation compares the current value with the new one and, if they are different, the target object is mutated, and the observer gets notified. When the observer is invoked, we pass an object literal that contains information related to the change (the name of the property, the previous value, and the current value).
This is a simplified implementation of the Change Observer pattern. More advanced implementations support multiple observers and use more traps to catch other types of mutation, such as field deletions or changes of prototype. Moreover, our implementation does not recursively create proxies for nested objects or arrays—a more advanced implementation takes care of these cases as well.
Let's see now how we can take advantage of observable objects with a trivial invoice application where the invoice total is updated automatically based on observed changes in the various fields of the invoice:
import { createObservable } from './create-observable.js'
function calculateTotal (invoice) { // (1)
return invoice.subtotal -
invoice.discount +
invoice.tax
}
const invoice = {
subtotal: 100,
discount: 10,
tax: 20
}
let total = calculateTotal(invoice)
console.log(`Starting total: ${total}`)
const obsInvoice = createObservable( // (2)
invoice,
({ prop, prev, curr }) => {
total = calculateTotal(invoice)
console.log(`TOTAL: ${total} (${prop} changed: ${prev} -> ${curr})`)
}
)
// (3)
obsInvoice.subtotal = 200 // TOTAL: 210
obsInvoice.discount = 20 // TOTAL: 200
obsInvoice.discount = 20 // no change: doesn't notify
obsInvoice.tax = 30 // TOTAL: 210
console.log(`Final total: ${total}`)
In the previous example, an invoice is composed of a subtotal
value, a discount
value, and a tax
value. The total amount can be calculated from these three values. Let's discuss the implementation in greater detail:
invoice
object and a value to hold the total
for it.invoice
object. Every time there is a change in the original invoice object, we recalculate the total and we also print some logs to keep track of the changes.obsInvoice
object the observer function is triggered, the total gets updated, and some logs are printed on the screen.If we run this example, we will see the following output in the console:
Starting total: 110
TOTAL: 210 (subtotal changed: 100 -> 200)
TOTAL: 200 (discount changed: 10 -> 20)
TOTAL: 210 (tax changed: 20 -> 30)
Final total: 210
In this example, we could make the total calculation logic arbitrarily complicated, for instance, by introducing new fields in the computation (shipping costs, other taxes, and so on). In this case, it will be fairly trivial to introduce the new fields in the invoice
object and update the calculateTotal()
function. Once we do that, every change to the new properties will be observed and the total
will be kept up to date with every change.
Observables are the cornerstone of reactive programming (RP) and functional reactive programming (FRP). If you are curious to know more about these styles of programming check out the Reactive Manifesto, at nodejsdp.link/reactive-manifesto.
The Proxy pattern and more specifically the Change Observer pattern are widely adopted patterns, which can be found on backend projects and libraries as well as in the frontend world. Some popular projects that take advantage of these patterns include the following:
Proxy
object.Proxy
object.Decorator is a structural design pattern that consists in dynamically augmenting the behavior of an existing object. It's different from classical inheritance, because the behavior is not added to all the objects of the same class, but only to the instances that are explicitly decorated.
Implementation-wise, it is very similar to the Proxy pattern, but instead of enhancing or modifying the behavior of the existing interface of an object, it augments it with new functionalities, as described in Figure 8.2:
Figure 8.2: Decorator pattern schematic
In Figure 8.2, the Decorator
object is extending the Component
object by adding the methodC()
operation. The existing methods are usually delegated to the decorated object without further processing but, in some cases, they might also be intercepted and augmented with extra behaviors.
Although proxy and decorator are conceptually two different patterns with different intents, they practically share the same implementation strategies. We will review them shortly. This time we want to use the Decorator pattern to be able to take an instance of our StackCalculator
class and "decorate it" so that it also exposes a new method called add()
, which we can use to perform additions between two numbers. We will also use the decorator to intercept all the calls to the divide()
method and implement the same division-by-zero check that we already saw in our SafeCalculator
example.
Using composition, the decorated component is wrapped around a new object that usually inherits from it. The decorator in this case simply needs to define the new methods, while delegating the existing ones to the original component:
class EnhancedCalculator {
constructor (calculator) {
this.calculator = calculator
}
// new method
add () {
const addend2 = this.getValue()
const addend1 = this.getValue()
const result = addend1 + addend2
this.putValue(result)
return result
}
// modified method
divide () {
// additional validation logic
const divisor = this.calculator.peekValue()
if (divisor === 0) {
throw Error('Division by 0')
}
// if valid delegates to the subject
return this.calculator.divide()
}
// delegated methods
putValue (value) {
return this.calculator.putValue(value)
}
getValue () {
return this.calculator.getValue()
}
peekValue () {
return this.calculator.peekValue()
}
clear () {
return this.calculator.clear()
}
multiply () {
return this.calculator.multiply()
}
}
const calculator = new StackCalculator()
const enhancedCalculator = new EnhancedCalculator(calculator)
enhancedCalculator.putValue(4)
enhancedCalculator.putValue(3)
console.log(enhancedCalculator.add()) // 4+3 = 7
enhancedCalculator.putValue(2)
console.log(enhancedCalculator.multiply()) // 7*2 = 14
If you remember our composition implementation for the Proxy pattern, you can probably see that the code here looks quite similar.
We created the new add()
method and enhanced the behavior of the original divide()
method (effectively replicating the feature we saw in the previous SafeCalculator
example). Finally, we delegated the putValue()
, getValue()
, peekValue()
, clear()
, and multiply()
methods to the original subject.
Object decoration can also be achieved by simply attaching new methods directly to the decorated object (monkey patching), as follows:
function patchCalculator (calculator) {
// new method
calculator.add = function () {
const addend2 = calculator.getValue()
const addend1 = calculator.getValue()
const result = addend1 + addend2
calculator.putValue(result)
return result
}
// modified method
const divideOrig = calculator.divide
calculator.divide = () => {
// additional validation logic
const divisor = calculator.peekValue()
if (divisor === 0) {
throw Error('Division by 0')
}
// if valid delegates to the subject
return divideOrig.apply(calculator)
}
return calculator
}
const calculator = new StackCalculator()
const enhancedCalculator = patchCalculator(calculator)
// ...
Note that in this example, calculator
and enhancedCalculator
reference the same object (calculator == enhancedCalculator
). This is because patchCalculator()
is mutating the original calculator
object and then returning it. You can confirm this by invoking calculator.add()
or calculator.divide()
.
It's possible to implement object decoration by using the Proxy
object. A generic example might look like this:
const enhancedCalculatorHandler = {
get (target, property) {
if (property === 'add') {
// new method
return function add () {
const addend2 = target.getValue()
const addend1 = target.getValue()
const result = addend1 + addend2
target.putValue(result)
return result
}
} else if (property === 'divide') {
// modified method
return function () {
// additional validation logic
const divisor = target.peekValue()
if (divisor === 0) {
throw Error('Division by 0')
}
// if valid delegates to the subject
return target.divide()
}
}
// delegated methods and properties
return target[property]
}
}
const calculator = new StackCalculator()
const enhancedCalculator = new Proxy(
calculator,
enhancedCalculatorHandler
)
// ...
If we were to compare these different implementations, the same caveats discussed during the analysis of the Proxy pattern would also apply for the decorator. Let's focus instead on practicing the pattern with a real-life example!
Before we start coding the next example, let's say a few words about LevelUP, the module that we are now going to work with.
LevelUP (nodejsdp.link/levelup) is a Node.js wrapper around Google's LevelDB, a key-value store originally built to implement IndexedDB in the Chrome browser, but it's much more than that. LevelDB has been defined as the "Node.js of databases" because of its minimalism and extensibility. Like Node.js, LevelDB provides blazingly fast performance and only the most basic set of features, allowing developers to build any kind of database on top of it.
The Node.js community, and in this case Rod Vagg, did not miss the chance to bring the power of this database into the Node.js world by creating LevelUP. Born as a wrapper for LevelDB, it then evolved to support several kinds of backends, from in-memory stores, to other NoSQL databases such as Riak and Redis, to web storage engines such as IndexedDB and localStorage, allowing us to use the same API on both the server and the client, opening up some really interesting scenarios.
Today, there is a vast ecosystem around LevelUP made of plugins and modules that extend the tiny core to implement features such as replication, secondary indexes, live updates, query engines, and more. Complete databases were also built on top of LevelUP, including CouchDB clones such as PouchDB (nodejsdp.link/pouchdb), and even a graph database, LevelGraph (nodejsdp.link/levelgraph), which can work both on Node.js and the browser!
Find out more about the LevelUP ecosystem at nodejsdp.link/awesome-level.
In the next example, we are going to show you how we can create a simple plugin for LevelUP using the Decorator pattern, and in particular, the object augmentation technique, which is the simplest but also the most pragmatic and effective way to decorate objects with additional capabilities.
For convenience, we are going to use the level
package (nodejsdp.link/level), which bundles both levelup
and the default adapter called leveldown
, which uses LevelDB as the backend.
What we want to build is a plugin for LevelUP that allows us to receive notifications every time an object with a certain pattern is saved into the database. For example, if we subscribe to a pattern such as {a: 1}
, we want to receive a notification when objects such as {a: 1, b: 3}
or {a: 1, c: 'x'}
are saved into the database.
Let's start to build our small plugin by creating a new module called level-subscribe.js
. We will then insert the following code:
export function levelSubscribe (db) {
db.subscribe = (pattern, listener) => { // (1)
db.on('put', (key, val) => { // (2)
const match = Object.keys(pattern).every(
k => (pattern[k] === val[k]) // (3)
)
if (match) {
listener(key, val) // (4)
}
})
}
return db
}
That's it for our plugin; it's extremely simple. Let's briefly analyze the preceding code:
db
object with a new method named subscribe()
. We simply attach the method directly to the provided db
instance (object augmentation).put
operation performed on the database.Let's now write some code to try out our new plugin:
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import level from 'level'
import { levelSubscribe } from './level-subscribe.js'
const __dirname = dirname(fileURLToPath(import.meta.url))
const dbPath = join(__dirname, 'db')
const db = level(dbPath, { valueEncoding: 'json' }) // (1)
levelSubscribe(db) // (2)
db.subscribe( // (3)
{ doctype: 'tweet', language: 'en' },
(k, val) => console.log(val)
)
db.put('1', { // (4)
doctype: 'tweet',
text: 'Hi',
language: 'en'
})
db.put('2', {
doctype: 'company',
name: 'ACME Co.'
})
This is how the preceding code works:
db
object.subscribe()
method, where we specify that we are interested in all the objects with doctype: 'tweet'
and language: 'en'
.put
. The first call triggers the callback associated with our subscription and we should see the stored object printed to the console. This is because, in this case, the object matches the subscription. The second call does not generate any output because the stored object does not match the subscription criteria.This example shows a real application of the Decorator pattern in its simplest implementation, which is object augmentation. It may look like a trivial pattern, but it has undoubted power if used appropriately.
For simplicity, our plugin works only in combination with put
operations, but it can be easily expanded to work even with batch
operations (nodejsdp.link/levelup-batch).
For more examples of how decorators are used in the real world, you can inspect the code of some more LevelUP plugins:
level-inverted-index
(nodejsdp.link/level-inverted-index): This is a plugin that adds inverted indexes to a LevelUP database, allowing us to perform simple text searches across the values stored in the databaselevelplus
(nodejsdp.link/levelplus): This is a plugin that adds atomic updates to a LevelUP databaseAside from LevelUP plugins, the following projects are also good examples of the adoption of the Decorator pattern:
json-socket
(nodejsdp.link/json-socket): This module makes it easier to send JSON data over a TCP (or a Unix) socket. It is designed to decorate an existing instance of net.Socket
, which gets enriched with additional methods and behaviors.fastify
(nodejsdp.link/fastify) is a web application framework that exposes an API to decorate a Fastify server instance with additional functionality or configuration. With this approach, the additional functionality is made accessible to different parts of the application. This is a quite generalized implementation of the Decorator pattern. Check out the dedicated documentation page to find out more at nodejsdp.link/fastify-decorators.At this point in the book, you might have some legitimate doubts about the differences between the Proxy and the Decorator patterns. These two patterns are indeed very similar and they can sometimes be used interchangeably.
In its classic incarnation, the Decorator pattern is defined as a mechanism that allows us to enhance an existing object with new behavior, while the Proxy pattern is used to control access to a concrete or virtual object.
There is a conceptual difference between the two patterns, and it's mostly based on the way they are used at runtime.
You can look at the Decorator pattern as a wrapper; you can take different types of objects and decide to wrap them with a decorator to enhance their capabilities with extra functionality. A proxy, instead, is used to control the access to an object and it does not change the original interface. For this reason, once you have created a proxy instance, you can pass it over to a context that expects the original object.
When it comes to implementation, these differences are generally much more obvious with strongly typed languages where the type of the objects you pass around is checked at compile time. In the Node.js ecosystem, given the dynamic nature of the JavaScript language, the line between the Proxy and the Decorator patterns is quite blurry, and often the two names are used interchangeably. We have also seen how the same techniques can be used to implement both patterns.
When dealing with JavaScript and Node.js, our advice is to avoid getting bogged down with the nomenclature and the canonical definition of these two patterns. We encourage you to look at the class of problems that proxy and decorator solve as a whole and treat these two patterns as complementary and sometimes interchangeable tools.
The Adapter pattern allows us to access the functionality of an object using a different interface.
A real-life example of an adapter would be a device that allows you to plug a USB Type-A cable into a USB Type-C port. In a generic sense, an adapter converts an object with a given interface so that it can be used in a context where a different interface is expected.
In software, the Adapter pattern is used to take the interface of an object (the adaptee) and make it compatible with another interface that is expected by a given client. Let's have a look at Figure 8.3 to clarify this idea:
Figure 8.3: Adapter pattern schematic
In Figure 8.3, we can see how the adapter is essentially a wrapper for the adaptee, exposing a different interface. The diagram also highlights the fact that the operations of the adapter can also be a composition of one or more method invocations on the adaptee. From an implementation perspective, the most common technique is composition, where the methods of the adapter provide a bridge to the methods of the adaptee. This pattern is pretty straightforward, so let's immediately work on an example.
We are now going to build an adapter around the LevelUP API, transforming it into an interface that is compatible with the core fs
module. In particular, we will make sure that every call to readFile()
and writeFile()
will translate into calls to db.get()
and db.put()
. This way we will be able to use a LevelUP database as a storage backend for simple filesystem operations.
Let's start by creating a new module named fs-adapter.js
. We will begin by loading the dependencies and exporting the createFsAdapter()
factory that we are going to use to build the adapter:
import { resolve } from 'path'
export function createFSAdapter (db) {
return ({
readFile (filename, options, callback) {
// ...
},
writeFile (filename, contents, options, callback) {
// ...
}
})
}
Next, we will implement the readFile()
function inside the factory and ensure that its interface is compatible with the one of the original function from the fs
module:
readFile (filename, options, callback) {
if (typeof options === 'function') {
callback = options
options = {}
} else if (typeof options === 'string') {
options = { encoding: options }
}
db.get(resolve(filename), { // (1)
valueEncoding: options.encoding
},
(err, value) => {
if (err) {
if (err.type === 'NotFoundError') { // (2)
err = new Error(`ENOENT, open "${filename}"`)
err.code = 'ENOENT'
err.errno = 34
err.path = filename
}
return callback && callback(err)
}
callback && callback(null, value) // (3)
})
}
In the preceding code, we had to do some extra work to make sure that the behavior of our new function is as close as possible to the original fs.readFile()
function. The steps performed by the function are described as follows:
db
instance, we invoke db.get()
, using filename
as a key, by making sure to always use its full path (using resolve()
). We set the value of the valueEncoding
option used by the database to be equal to any eventual encoding
option received as an input.ENOENT
as the error code, which is the code used by the original fs
module to indicate a missing file. Any other type of error is forwarded to callback
(for the scope of this example, we are adapting only the most common error condition).callback
.The function that we created does not want to be a perfect replacement for the fs.readFile()
function, but it definitely does its job in the most common situations.
To complete our small adapter, let's now see how to implement the writeFile()
function:
writeFile (filename, contents, options, callback) {
if (typeof options === 'function') {
callback = options
options = {}
} else if (typeof options === 'string') {
options = { encoding: options }
}
db.put(resolve(filename), contents, {
valueEncoding: options.encoding
}, callback)
}
As we can see, we don't have a perfect wrapper in this case either. We are ignoring some options such as file permissions (options.mode
), and we are forwarding any error that we receive from the database as is.
Our new adapter is now ready. If we now write a small test module, we can try to use it:
import fs from 'fs'
fs.writeFile('file.txt', 'Hello!', () => {
fs.readFile('file.txt', { encoding: 'utf8' }, (err, res) => {
if (err) {
return console.error(err)
}
console.log(res)
})
})
// try to read a missing file
fs.readFile('missing.txt', { encoding: 'utf8' }, (err, res) => {
console.error(err)
})
The preceding code uses the original fs
API to perform a few read and write operations on the filesystem, and should print something like the following to the console:
Error: ENOENT, open "missing.txt"
Hello!
Now, we can try to replace the fs
module with our adapter, as follows:
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import level from 'level'
import { createFSAdapter } from './fs-adapter.js'
const __dirname = dirname(fileURLToPath(import.meta.url))
const db = level(join(__dirname, 'db'), {
valueEncoding: 'binary'
})
const fs = createFSAdapter(db)
// ...
Running our program again should produce the same output, except for the fact that no parts of the file that we specified are read or written using the filesystem API directly. Instead, any operation performed using our adapter will be converted into an operation performed on a LevelUP database.
The adapter that we just created might look silly; what's the purpose of using a database in place of the real filesystem? However, we should remember that LevelUP itself has adapters that enable the database to also run in the browser. One of these adapters is level-js
(nodejsdp.link/level-js). Now our adapter makes perfect sense. We could use something similar to allow code leveraging the fs
module to run on both Node.js and a browser. We will soon realize that Adapter is an extremely important pattern when it comes to sharing code with the browser, as we will see in more detail in Chapter 10, Universal JavaScript for Web Applications.
There are plenty of real-world examples of the Adapter pattern. We've listed some of the most notable examples here for you to explore and analyze:
level-filesystem
(nodejsdp.link/level-filesystem), which is the proper implementation of the fs
API on top of LevelUP.Structural design patterns are definitely some of the most widely adopted design patterns in software engineering and it is important to be confident with them. In this chapter, we explored the Proxy, the Decorator, and the Adapter patterns and we discussed different ways to implement these in the context of Node.js.
We saw how the Proxy pattern can be a very valuable tool to control access to existing objects. In this chapter, we also mentioned how the Proxy pattern can enable different programming paradigms such as reactive programming using the Change Observer pattern.
In the second part of the chapter, we found out that the Decorator pattern is an invaluable tool to be able to add additional functionality to existing objects. We saw that its implementation doesn't differ much from the Proxy pattern and we explored some examples built around the LevelDB ecosystem.
Finally, we discussed the Adapter pattern, which allows us to wrap an existing object and expose its functionality through a different interface. We saw that this pattern can be useful to expose a piece of existing functionality to a component that expects a different interface. In our examples, we saw how this pattern can be used to implement an alternative storage layer that is compatible with the interface provided by the fs
module to interact with files.
Proxy, decorator and adapter are very similar, the difference between them can be appreciated from the perspective of the interface consumer: proxy provides the same interface as the wrapped object, decorator provides an enhanced interface, and adapter provides a different interface.
In the next chapter, we will complete our journey through traditional design patterns in Node.js by exploring the category of behavioral design patterns. This category includes important patterns such as the Strategy pattern, the Middleware pattern, and the Iterator pattern. Are you ready to discover behavioral design patterns?
superagent-cache
module (nodejsdp.link/superagent-cache).console
object that enhances every logging function (log()
, error()
, debug()
, and info()
) by prepending the current timestamp to the message you want to print in the logs. For instance, executing consoleProxy.log('hello')
should print something like 2020-02-18T15:59:30.699Z hello
in the console.red(message)
, yellow(message)
, and green(message)
methods. These methods will have to behave like console.log(message)
except they will print the message in red, yellow, or green, respectively. In one of the exercises from the previous chapter, we already pointed you to some useful packages to to create colored console output. If you want to try something different this time, have a look at ansi-styles
(nodejsdp.link/ansi-styles).Map
instance to store the key-value pairs of filenames and the associated data.createLazyBuffer(size)
, a factory function that generates a virtual proxy for a Buffer
of the given size? The proxy instance should instantiate a Buffer
object (effectively allocating the given amount of memory) only when write()
is being invoked for the first time. If no attempt to write into the buffer is made, no Buffer
instance should be created.18.220.108.111