Proxy

A proxy is an object that controls access to another object, called a subject. The proxy and the subject have an identical interface and this allows us to transparently swap one for the other; 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. The following figure shows a diagrammatic representation:

Proxy

The preceding figure shows us how the Proxy and the Subject have the same interface, and how this is totally 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 pre-processing or post-processing.

Note

It's important to observe that we are not talking about proxying between classes; the proxy pattern involves wrapping actual instances of the subject, thus preserving its state.

A proxy is useful in several circumstances; for example, consider the following ones:

  • 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 operations are executed on the subject only if the data is not yet present in the cache
  • Lazy initialization: If the creation of the subject is expensive, the proxy can delay it to when it's really necessary
  • Logging: The proxy intercepts the method invocations and the relative parameters, recoding them as they happen
  • Remote objects: A proxy can take an object that is located remotely, and make it appear local

Of course, there are many more applications for the proxy pattern, but these should give us an idea of the extent 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 of them directly to the subject. There are several ways in which this can be achieved; let's analyze some of them.

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 is one example of this technique using a pseudo class and a factory:

function createProxy(subject) { 
  const proto = Object.getPrototypeOf(subject); 
 
  function Proxy(subject) { 
    this.subject = subject; 
  } 
 
  Proxy.prototype = Object.create(proto); 
 
  //proxied method 
  Proxy.prototype.hello = function(){ 
    return this.subject.hello() + ' world!'; 
  }; 
 
  //delegated method 
  Proxy.prototype.goodbye = function(){ 
    return this.subject.goodbye 
      .apply(this.subject, arguments); 
  }; 
 
  return new Proxy(subject); 
}
module.exports = createProxy; 

To implement a proxy using composition, we have to intercept the methods that we are interested in manipulating (such as hello()), while simply delegating the rest of them to the subject (as we did with goodbye()).

The preceding code also shows the particular case where the subject has a prototype and we want to maintain the correct prototype chain, so that executing proxy instanceof Subject will return true; we used pseudo-classical inheritance to achieve this.

This is just an extra step, required only if we are interested in maintaining the prototype chain, which can be useful in order to improve the compatibility of the proxy with code initially meant to work with the subject.

However, as JavaScript has dynamic typing, most of the time we can avoid using inheritance and use more immediate approaches. For example, an alternative implementation of the proxy presented in the preceding code might just use an object literal and a factory:

function createProxy(subject) { 
  return { 
    //proxied method 
    hello: () => (subject.hello() + ' world!'), 
 
    //delegated method 
    goodbye: () => (subject.goodbye.apply(subject, arguments)) 
  }; 
} 

Note

If we want to create a proxy that delegates most of its methods, it would be convenient to generate these automatically using a library, such as delegates (https://npmjs.org/package/delegates).

Object augmentation

Object augmentation (or monkey patching) is probably the most pragmatic way of proxying individual methods of an object and consists of modifying the subject directly by replacing a method with its proxied implementation; consider the following example:

function createProxy(subject) { 
  const helloOrig = subject.hello; 
  subject.hello = () => (helloOrig.call(this) + ' world!'); 

  return subject; 
} 

This technique is definitely the most convenient one when we need to proxy only one or a few methods, but it has the drawback of modifying the subject object directly.

A comparison of the different techniques

Composition can be considered the safest 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. If needed, we might also have to delegate the access to the properties of the subject.

Tip

The object properties can be delegated using Object.defineProperty(). Find out more at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty.

Object augmentation, on the other hand, modifies the subject, which might not always be what we want, but it does not present the various inconveniences related to delegation. For this reason, object augmentation is definitely the most pragmatic way to implement proxies in JavaScript, and it's the preferred technique in all those circumstances where modifying the subject is not a big concern.

However, there is at least one situation where composition is almost necessary; this is when we want to control the initialization of the subject, to, for example, create it only when needed (lazy initialization).

Tip

It is worth pointing out that by using a factory function (createProxy() in our examples), we can shield our code from the technique used to generate the proxy.

Creating a logging Writable stream

To see the proxy pattern in a real example, we will now build an object that acts as a proxy to a Writable stream, by intercepting all the calls to the write() method and logging a message every time this happens. We will use an object composition to implement our proxy; this is how the loggingWritable.js file looks:

function createLoggingWritable(writableOrig) { 
  const proto = Object.getPrototypeOf(writableOrig); 
 
  function LoggingWritable(writableOrig) { 
    this.writableOrig = writableOrig; 
  } 
 
  LoggingWritable.prototype = Object.create(proto); 
 
  LoggingWritable.prototype.write = function(chunk, encoding, callback) { 
    if(!callback && typeof encoding === 'function') { 
      callback = encoding; 
      encoding = undefined; 
    } 
    console.log('Writing ', chunk); 
    return this.writableOrig.write(chunk, encoding, function() { 
      console.log('Finished writing ', chunk); 
      callback && callback(); 
    }); 
  }; 
 
  LoggingWritable.prototype.on = function() { 
    return this.writableOrig.on 
      .apply(this.writableOrig, arguments); 
  }; 
 
  LoggingWritable.prototype.end = function() { 
    return this.writableOrig.end 
      .apply(this.writableOrig, arguments); 
  }; 
 
  return new LoggingWritable(writableOrig); 
} 

In the preceding code, we created a factory that returns a proxied version of the writable object passed as an argument. We provide an override for the write() method that logs a message to the standard output every time it is invoked and every time the asynchronous operation completes. This is also a good example of creating proxies of asynchronous functions, which makes proxying the callback necessary as well; this is an important detail to be considered in a platform such as Node.js. The remaining methods, on() and end(), are simply delegated to the original writable stream (to keep the code leaner we are not considering the other methods of the writable interface).

We can now include a few more lines of code into the loggingWritable.js module to test the proxy that we just created:

const fs = require('fs'); 
 
const writable = fs.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 stream is transparently logged to the console.

Proxy in the ecosystem – function hooks and AOP

In its numerous forms, proxy is quite a popular pattern in Node.js, as well as in the ecosystem. In fact, we can find several libraries that allow us to simplify the creation of proxies, most of the time leveraging object augmentation as an implementation approach. In the community, this pattern can also be referred to as function hooking, or sometimes as Aspect-Oriented Programming (AOP), which is actually a common area of application for proxies. In AOP, these libraries usually allow the developer to set pre- or post-execution hooks for a specific method (or a set of methods) that allow us to execute custom code before and after the execution of the advised method, respectively.

Sometimes proxies are also called middleware, because, as it happens in the middleware pattern (which we will see later in the chapter), they allow us to pre-process and post-process the input/output of a function. Sometimes, they also allow the registering of multiple hooks for the same method using a middleware-like pipeline.

There are several libraries on npm that allow us to implement function hooks with little effort. Among them there are hooks (https://npmjs.org/package/hooks), hooker (https://npmjs.org/package/hooker), and meld (https://npmjs.org/package/meld).

ES2015 Proxy

The ES2015 specification introduced a global object called Proxy, which is available in Node.js starting from version 6.

The Proxy API contains 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 an example:

const scientist = { 
  name: 'nikola', 
  surname: 'tesla' 
}; 
 
const uppercaseScientist = new Proxy(scientist, { 
  get: (target, property) => target[property].toUpperCase() 
}); 
 
console.log(uppercaseScientist.name, uppercaseScientist.surname);  
  // prints NIKOLA TESLA 

In this example, we are using the Proxy API to intercept all access to the properties of the target object, scientist, and convert the original value of the property to an uppercase string.

If you look carefully at this example, you can probably notice something very peculiar about this API: it allows us to intercept access to the generic attribute in the target object. This is possible because the API is not just a simple wrapper to facilitate the creation of proxy objects, like the ones we defined in the previous sections of this chapter; instead it 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 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 the 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.

Looking at the implementation, this proxy uses an empty array as the target and then defines the traps get and has 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 API supports a number of 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 API provides a powerful foundation for taking advantage of the Proxy design pattern when you need it.

Tip

If you are curious to know more about the Proxy API and discover all its capabilities and trap methods, you can read more in this article by Mozilla: https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Global_Objects/Proxy. Another good source is this detailed article from Google: https://developers.google.com/web/updates/2016/02/es2015-proxies.

In the wild

Mongoose (http://mongoosejs.com) is a popular Object-Document Mapping (ODM) library for MongoDB. Internally, it uses the hooks package (https://npmjs.org/package/hooks) to provide pre-and post-execution hooks for the init, validate, save, and remove methods of its Document objects. Find out more with the official documentation at http://mongoosejs.com/docs/middleware.html.

..................Content has been hidden....................

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