8

Structural Design Patterns

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:

  • Proxy: A pattern that allows us to control access to another object
  • Decorator: A common pattern to augment the behavior of an existing object dynamically
  • Adapter: A pattern that allows us to access the functionality of an object using a different interface

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.

Proxy

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:

  • Data validation: The proxy validates the input before forwarding it to the subject
  • Security: The proxy verifies that the client is authorized to perform the operation, and it passes the request to the subject only if the outcome of the check is positive
  • Caching: The proxy keeps an internal cache so that the proxied operations are executed on the subject only if the data is not yet present in the cache
  • Lazy initialization: If creating the subject is expensive, the proxy can delay it until it's really necessary
  • Logging: The proxy intercepts the method invocations and the relative parameters, recoding them as they happen
  • Remote objects: The proxy can take a remote object and make it appear local

There are more Proxy pattern applications, but these should give us an idea of its purpose.

Techniques for implementing proxies

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.

Object composition

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

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 built-in Proxy object

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.

Additional capabilities and limitations of the Proxy object

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:

  • The get trap intercepts access to the array elements, returning the even number for the given index
  • The has trap instead intercepts the usage of the in operator and checks whether the given number is even or not

The 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.

A comparison of the different proxying techniques

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.

Creating a logging Writable stream

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:

  1. We create and return a proxy for the original writable object using the ES2015 Proxy constructor.
  2. We use the get trap to intercept access to the object properties.
  3. We check whether the property accessed is the write method. If that is the case, we return a function to proxy the original behavior.
  4. The proxy implementation logic here is simple: we extract the current 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.
  5. We return unchanged any other property.

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.

Change observer with Proxy

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:

  1. We declare a function that calculates the total for a given invoice, then we create an invoice object and a value to hold the total for it.
  2. Here we create an observable version of the 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.
  3. Finally, we apply some changes to the observable invoice. Every time we mutate the 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.

In the wild

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:

  • LoopBack (nodejsdp.link/loopback) is a popular Node.js web framework that uses the Proxy pattern to provide the capability to intercept and enhance method calls on controllers. This capability can be used to build custom validation or authentication mechanisms.
  • Version 3 of Vue.js (nodejsdp.link/vue), a very popular JavaScript reactive UI framework, has reimplemented observable properties using the Proxy pattern with the Proxy object.
  • MobX (nodejsdp.link/mobx) is a famous reactive state management library commonly used in frontend applications in combination with React or Vue.js. Like Vue.js, MobX implements reactive observables using the Proxy object.

Decorator

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:

A screenshot of a cell phone

Description automatically generated

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.

Techniques for implementing decorators

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.

Composition

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 augmentation

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().

Decorating with the Proxy object

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!

Decorating a LevelUP database

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.

Introducing LevelUP and LevelDB

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.

Implementing a LevelUP plugin

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:

  1. We decorate the db object with a new method named subscribe(). We simply attach the method directly to the provided db instance (object augmentation).
  2. We listen for any put operation performed on the database.
  3. We perform a very simple pattern-matching algorithm, which verifies that all the properties in the provided pattern are also available in the data being inserted.
  4. If we have a match, we notify the listener.

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:

  1. First, we initialize our LevelUP database, choosing the directory where the files are stored and the default encoding for the values.
  2. Then, we attach our plugin, which decorates the original db object.
  3. At this point, we are ready to use the new feature provided by our plugin, which is the subscribe() method, where we specify that we are interested in all the objects with doctype: 'tweet' and language: 'en'.
  4. Finally, we save some values in the database using 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).

In the wild

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 database
  • levelplus (nodejsdp.link/levelplus): This is a plugin that adds atomic updates to a LevelUP database

Aside 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.

The line between proxy and decorator

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.

Adapter

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:

A screenshot of a cell phone

Description automatically generated

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.

Using LevelUP through the filesystem API

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:

  1. To retrieve a file from the 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.
  2. If the key is not found in the database, we create an error with 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).
  3. If the key-value pair is retrieved successfully from the database, we will return the value to the caller using the 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.

In the wild

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:

  • We already know that LevelUP is able to run with different storage backends, from the default LevelDB to IndexedDB in the browser. This is made possible by the various adapters that are created to replicate the internal (private) LevelUP API. Take a look at some of them to see how they are implemented at nodejsdp.link/level-stores.
  • JugglingDB is a multi-database ORM and of course, multiple adapters are used to make it compatible with different databases. Take a look at some of them at nodejsdp.link/jugglingdb-adapters.
  • nanoSQL (nodejsdp.link/nanosql) is a modern multi-model database abstraction library that makes heavy usage of the Adapter pattern to support a significant variety of databases.
  • The perfect complement to the example that we created is level-filesystem (nodejsdp.link/level-filesystem), which is the proper implementation of the fs API on top of LevelUP.

Summary

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?

Exercises

  • 8.1 HTTP client cache: Write a proxy for your favorite HTTP client library that caches the response of a given HTTP request, so that if you make the same request again, the response is immediately returned from the local cache, rather than being fetched from the remote URL. If you need inspiration, you can check out the superagent-cache module (nodejsdp.link/superagent-cache).
  • 8.2 Timestamped logs: Create a proxy for the 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.
  • 8.3 Colored console output: Write a decorator for the console that adds the 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).
  • 8.4 Virtual filesystem: Modify our LevelDB filesystem adapter example to write the file data in memory rather than in LevelDB. You can use an object or a Map instance to store the key-value pairs of filenames and the associated data.
  • 8.5 The lazy buffer: Can you implement 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.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.218.215