How to work with the proxy pattern in JavaScript

Photo by Andrew Neel on Unsplash

How to work with the proxy pattern in JavaScript

A deep dive into the proxy pattern

Introduction

This article unravels the workings of a popular structural design pattern called ‘the Proxy pattern’. You will understand the pattern's implementation, use cases, pitfalls, and benefits.

Prerequisite

To get a good read of this article, you should understand the basics of JavaScript.

Introduction to Design Patterns

What are design patterns?

Design patterns are concepts used to solve common recurring problems in software design. They help structure code in a common vocabulary that is easily understood. Design patterns are not exact solutions; they provide a generic solution scheme that can be used to solve problems in different situations. There are three types of design patterns:

  • Creational Design patterns are all about how objects are created(instantiated). Examples are Abstract factory, builder, factory method, object pool, singleton, and prototype.

  • Structural design patterns: These patterns are all about Class and Object composition. Structural patterns provide methods to compose objects with new functionality. They describe common ways of making classes and objects repeatable as solutions in larger structures. Examples are Adapters, Proxy, Bridge, Composite, Decorator, Façade, Flyweight, etc.

  • Behavioral design patterns: These patterns are concerned with communication between objects. They provide guidelines to solve commonly occurring object-interaction-related problems in software design. Examples are Chain of Responsibility, Command, Interpreter, Mediator, State, Visitor, Observer, Memento, Null object, Template method, etc.

Proxy patterns

The Proxy design pattern lets you use a “Proxy “(an Object) to intercept requests made when accessing objects. The proxy object intercepts the request and optionally forwards it to the target object. We commonly use the dot notation to access objects; we gain more control over an object with the proxy pattern, allowing us to add new functionality/perform certain operations before or after a request gets to it.

Image from: https://levelup.gitconnected.com/proxy-design-pattern-9090969c2dd6

Applications of proxy patterns

Here are some of the use cases of proxies in software development:

  • Validation: One great use case of proxies is to validate user input before setting it as a property. It ensures only valid data are in object properties.

  • Access control: Proxies can intercept and control access to objects or properties. It gives you more access control.

  • Lazy Loading: For performance optimization, use a proxy to load a resource only when it is accessed.

  • Caching: Proxies can also implement caching mechanisms by intercepting property access and returning cached values if available.

  • Dynamic Behavior Modification: Proxies can add behavior to existing objects without modifying their original implementation.

  • Immutable Objects: Proxies can create immutable objects by preventing property modification after being set.

There are many more use cases of this pattern in software architecture; the intercepting mechanism of proxies is powerful when used correctly.

Implementation of Proxy pattern in JavaScript

The Proxy Object

Lucky for us, we have a built-in Proxy object in JavaScript, unlike many other languages. The proxy object allows you to create a proxy for another object, which can intercept and redefine operations on that object.

Syntax and description

The proxy object acts like a clone of the original object. It can redefine fundamental object operations like getting, setting, and redefining properties.

The proxy object takes in two parameters:

  • Target object: The original object you want a proxy of.

  • Handler object: An object that defines which operations will be intercepted and how to redefine such operations. It comes with built-in methods such as get and set.

For example:

const university = {
    name: "University of Lagos",
    place: "Yaba Lagos",
    Alias: "School of first choice"
};

const handler = {};

const proxy = new Proxy(university, handler);

In this example, the “university” object is the target object, and the handler object is empty, so the proxy behaves exactly like the original object.


console.log(proxy.name, ',', proxy.place, ',', proxy.Alias) 
// result is university of lagos, Yaba Lagos, school of first choice

The proxy now behaves like a clone of the object. Adding more functionality involves modifying the handler object. Let’s see an example:


const university = {
    name: "university of lagos",
    place: "Yaba Lagos",
    Alias: "school of first choice"
}

const handler = {
    get: (target,prop, receiver) => {
        return "intercepted"
    }
}

const proxy = new Proxy(university, handler)
console.log(proxy.name)
// result is intercepted

Here, we added a handler function “get”, which intercepts attempts to access properties in the target object. The following parameters are passed to it:

  • The target object

  • Property: The name of the object’s property that needs to be accessed

  • Receiver: The proxy or an object inheriting from the proxy

Any value returned in the get function gets logged when attempting to access the object’s property. This is why “intercepted” is logged to the console.

The “THIS” keyword is bound to the handler’s object itself; for example:

const handler2 = {
  num: 50000,

  get: function () {
    return this.num; // 'this' refers to the handler object
  },
};

const proxy2 = new Proxy({regnumber: 200912}, handler2);

console.log(proxy2.regnumber)

Let’s take a look at a more elaborate example:

const book = {
  name: "JAVASCRIPT DESIGN PATTERNS",
  author: "OLADELE JUWON",
  publication: "walker's publication",
};

const bookproxy = new Proxy(book, {
  get: (target, prop) => {
    if (prop === "name") {
      console.log(`The name of the book is "${target[prop]}" `);
      return target[prop];
    } else {
      return target[prop];
    }
  },
});

console.log(bookproxy.name);
//result is JAVASCIPT DESIGN PATTERNS

This example does a little validation. Here, in the get handler function, we use a condition to check the property accessed; we conditionally log a string to the console if the property value is "name" before returning a value, which is the target[prop]. " This accesses the property from the target object, and you get your value.

Apart from the get handler method, we also have a "set" method for setting object property values. Four parameters are passed to it:

  • Target object

  • The name of the property to be changed

  • The new value of the property to set

  • Receiver(optional): The object the set operation gets directed back to, usually the proxy itself, except in some cases where the set handler gets called indirectly.

The return value should be true or false, true if the operation was successful, and false if not.

Note: - The set handler intercepts property assignment operations, e.g.,

//property assignment operations
book.name = “juwon”;
book[name] = ‘john’;

Let’s take a look at the previous example with the set method added to it:

const book = {
  name: "JAVASCRIPT DESIGN PATTERNS",
  author: "OLADELE JUWON",
  publication: "walker's publication",
};

const bookproxy = new Proxy(book, {

  set: (target,prop,value) => {
    console.log(`${value}`)
    target[prop] = value;
    return true
  }
});
bookproxy.name = 'john'
console.log(bookproxy.name); //result is john

Here, the set handler function gets called when there is an attempt to set a new value for the 'name' property on the proxy. The value 'John' is passed and logged into the console before being set into the object. Subsequently, we return true, indicating a successful operation.

We have other methods on the proxy object. It is advisable to check the MDN documentation for more knowledge.

Reflect object

This global object contains static methods to modify and intercept JavaScript internal methods. All methods on this object are static methods (like the MATH method); these methods have the same names as the proxy handler methods, e.g., get, set, etc.

The major use case of reflect methods is in proxies. Let us see an application:

const person = {
    name: 'john',
    age: 10000,
    email: "juwom@email.com"
}
const personproxy = new Proxy(person, {
    get: (target,prop) => {
        return `the result of the query is ${Reflect.get(target,prop)}`
    }

})

console.log(personproxy.age) //result is 'the result of the query is 1000'

Here, we use the reflect method "get" to access the property value. This method takes three parameters: target, property, and receiver(optional). Let us take a look at the Reflect set method:

const person = {
    name: 'john',
    age: 10000,
    email: "juwom@email.com"
}

const personproxy = new Proxy(person, {
    get: (target,prop) => {
        return `the result of the query is ${Reflect.get(target,prop)}`
    },
    set: (target,prop,value) => {
        return Reflect.set(target,prop,value);
    }

})

personproxy.age = 50
console.log(personproxy.age) //result is 'the result of the query is 50'

The Reflect method “set” takes in four parameters:

  • Target object,

  • Name of the property to be set,

  • The new value of the property,

  • The receiver(optional),

The method returns a Boolean to indicate the success or failure of the operation, which is why it can be returned directly in the set handler function.

There are many more exciting methods for the Reflect object. It is highly advisable to check out the documentation to learn more about it.

Pros and cons of using the Proxy pattern

The Proxy design pattern is a structural design pattern that creates a placeholder object for another object to control its access. Proxies give us access control to an object; we can restrict certain operations and add functionality to them. This functionality is useful for validation, formatting, etc.

The intercepting mechanism of the proxy pattern is powerful, but it introduces complexities to our codebase. It can also cause performance issues due to the extra interactions performed per interception. Not all situations require proxies; to avoid complications, use proxies only when necessary.

Best practices when using this pattern

Here are a few best practices:

  • Define responsibility: - The proxy and the object shouldn’t have coinciding responsibilities; they should not perform the same tasks as the object.

  • Handle security: - Ensure the proxy handles access control and authentication effectively if security is a concern to avoid unwanted access to an object.

  • Do not over-engineer: - Use this pattern only when there is a clear need for control or additional functionality.

  • Use together with other design patterns to provide more elaborate solutions.

Following these best practices ensures your code aligns with the software engineering principles.

Conclusion

This article should have given you enough knowledge about the proxy pattern and how to implement it. Consider reading more on it via the documentation and exploring other design patterns.

Additional resources

More on the Proxy object

More on the Reflect object

Proxy