Proxy Pattern

Intercept and control interactions to target objects

Overview

The Proxy Pattern uses a Proxy to intercept and control interactions with target objects.

Let's say that we have a person object. We can access properties with either dot or bracket notation:

and modify property values in a similar fashion:

With the Proxy pattern, we don't want to interact with this object directly. Instead, a Proxy object intercepts the request and (optionally) forwards this to the target object - the person object in this case.


Implementation

In JavaScript, we can easily create a new proxy by using the built-in Proxy object.

Proxy Pattern

The Proxy object receives two arguments:

  1. The target object
  2. A handler object, which we can use to add functionality to the proxy. This object comes with some built-in functions that we can use, such as get and set.

The get method on the handler object gets invoked when we want to access a property, and the set method gets invoked when we want to modify a property.

proxy-implementation.js
const person = {
  name: "John Doe",
  age: 42,
  email: "john@doe.com",
  country: "Canada",
};

const personProxy = new Proxy(person, {
  get: (target, prop) => {
    console.log(`The value of ${prop} is ${target[prop]}`);
    return target[prop];
  },
  set: (target, prop, value) => {
    console.log(`Changed ${prop} from ${target[prop]} to ${value}`);
    target[prop] = value;
    return true;
  },
});

The Proxy object receives two arguments:

  1. The target object
  2. A handler object, which we can use to add functionality to the proxy. This object comes with some built-in functions that we can use, such as get and set. The get method on the handler object gets invoked when we want to access a property, and the set method gets invoked when we want to modify a property.
proxy-implementation.js
const person = {
  name: "John Doe",
  age: 42,
  email: "john@doe.com",
  country: "Canada",
};

const personProxy = new Proxy(person, {
  get: (target, prop) => {
    console.log(`The value of ${prop} is ${target[prop]}`);
    return target[prop];
  },
  set: (target, prop, value) => {
    console.log(`Changed ${prop} from ${target[prop]} to ${value}`);
    target[prop] = value;
    return true;
  },
});

Reflect

The built-in Reflect object makes it easier to manipulate the target object.

Instead of accessing properties through obj[prop] or setting properties through obj[prop] = value, we can access or modify properties on the target object through Reflect.get() and Reflect.set(). The methods receive the same arguments as the methods on the handler object.

Tradeoffs

Control: Proxies make it easy to add functionality when interacting with a certain object, such as validation, logging, formatting, notifications, debugging.
⚠️ Long handler execution: Executing handlers on every object interaction could lead to performance issues.

Exercise

Challenge

Add the following validation to the user object:

  • The username property has to be a string that only contains of letters, and is at least 3 characters long
  • The email property has to be a valid email address.
  • The age property has to be a number, and has to be at least 18
  • When a property is retrieved, change the output to ${new Date()} | The value of ${property}} is ${target[property]}. For example if we get user.name, it needs to log 2022-05-31T15:29:15.303Z | The value of name is John

Solution