ES6 Proxies
ES6 Proxies open up some easy meta programming options.
The ES6 Proxy
is now supported by most recent browser versions. A proxy wraps a target object and can intercept many of the operations on that target, for example: property access; method invocation; and construction. This opens up some simple, elegant patterns for extending the behaviour of a system without changing code, a neat way to embrace the "open/closed" principle. This post is just an brief exploration of a couple of those patterns.
Logging
It can be handy to add logging to your code. Rather than cracking open your existing (tested) code it is preferable to add logging as a separate concern. One simple pattern for doing that is to decorate existing function calls.
Let's say we had a function greet
on some object target
:
const target = {
greet: (name) => `Hello ${name}`,
};
target.greet('world');
// returns 'Hello world'
Using Proxy
we can add logging to target.greet
(and any other functions on target
) without touching its implementation; whenever a function on the target is accessed the proxy can return a function that performs the logging in addition to calling the original function:
const target = {
greet: (name) => `Hello ${name}`,
};
const decorateIfFunction = (obj) =>
typeof obj == 'function' ?
decorateWithLogging(obj) :
obj;
const decorateWithLogging = (fn) => (...args) => {
console.log(`called ${fn.name} with [${args}]`);
return fn(...args);
};
const proxy = new Proxy(target, {
get: (target, prop) =>
decorateIfFunction(target[prop]),
});
console.log( proxy.greet('world') );
// Logs "called greet with [world]",
// then "Hello world".
"Dirty" property tracking à la ORMs
If (for some reason) you were implementing an ORM library you might want to provide consumers of your library a simple API:
// Get a tracked `Person` instance
const person1 = orm.getById(Person, 1);
// Might look like: {id: 1, name: 'foo', pets: 3}
// Mutate some state using simple property assignment
person1.name = "Foo";
// Persist the changes
orm.update(person1);
// Will only update changed properties i.e. {name: 'Foo'}
That could be achieved quite simply by making the ORM return a proxy for each object. That proxy could have a special property to keep track of the dirty properties for use by the ORM:
function ORM() {
// We will maintain a special property on each instance to keep track of
// its dirty properties. The property holds an object where the keys are
// the dirty property keys.
const dirtyPropsSymbol = Symbol('Prop holding obj where keys are dirty props');
const resetDirtyProps = (instance) => instance[dirtyPropsSymbol] = {};
const markDirtyProp = (instance, prop) => instance[dirtyPropsSymbol][prop] = true;
const getDirtyProps = (instance) => Object.keys(instance[dirtyPropsSymbol]);
return {
// Get an instance of `Fn` with the id of `id`
getById: (Fn, id) => {
// In real version we would perhaps hit an API or DB, but here we'll
// just `new` an instance
const instance = new Fn();
// Wrap the instance in a proxy that tracks dirty props
resetDirtyProps(instance);
return new Proxy(instance, {
set: (target, property, value) => {
markDirtyProp(instance, property);
// Perform the actual property assignment
target[property] = value;
return true;
},
});
},
// Persist and changes made on `instance`
update: (instance) => {
// In real version we'd perform the necessary updates to
// persist any "dirty" properties, here we just log them
const dirtyPropertyNames = getDirtyProps(instance);
dirtyPropertyNames.length === 0 ?
console.log('Nothing to update') :
console.log('Dirty properties:', dirtyPropertyNames);
// All properties are now persisted, so clear the list
resetDirtyProps(instance);
},
};
}