← See all articlesJoris PISSONJoris PISSON - Feb 14, 2023

A journey through frontend Aspect-Oriented Programming - Proxies

This paper about Aspect-Oriented Programming (AOP) covers common use cases and thoughts around it. Last week we have covered some theories and thoughts about AOP. Today we will focus on Proxies.

Aspect Oriented Programming applied to Frontend

NB: Some examples are a demo of what you should not do.

TLDR;

AOP is a powerful tool that you should use cautiously. I strongly recommend you read some papers on Proxy, Reflect API, and incoming Decorators. Proxies are versatile, it's a great way to enhance an external library or your own code.

Here is the link to the demo app repository; There is an overuse of AOP for a didactic purpose and obviously, this App could be improved.

First, we need some reminders about Proxies and Reflect standard API.

Proxies

The Proxy object allows you to create an object that can be used as the original object, but which may redefine fundamental Object operations like getting, setting, and defining properties.

const proxy = new Proxy(target, handler)

Target: Original target that we want to coat with a new feature. It can be an Object, an Array, a Function, and even another Proxy.

Handler: An object that has to implement some function defining the behavior of the Proxy. We will use mostly get, set, and has; although there are more functions that you could need. [Proxy spec]

Reflect: In combination with Proxies, it's also really useful to deepen your knowledge on reflection, to use them into your handlers. [Reflect spec]

You should always use the Reflect API in your traps (handlers), even if it seems easier to use indirect property access; it will avoid corner cases. And here are the limitations you should have in mind when you create proxies.

How does it fit in my App?

Data layer

Your state manager like Vuex / Redux can be a good candidate due to its structure.

Github code

If you spread this Object, you can easily add a Proxy on top of your actions and/or mutations. But a lot of web app doesn't need a store…

Service layer

You can put your Aspect in your business class instances.

In both cases, your Aspects are defined once and applied wherever you need them.

Credit to "souryoukomi"

Stop teasing, where do we start?

Here are some baby steps into proxies.

Github code

When we access a property on the proxy, the call is intercepted by the proxy handler and gives us some information in the console on the target and the property we are accessing, before the Reflection on the target returns the property value.

The good, the bad, and the ugly

The good, the bad and the ugly, citation painted on an helmet.

NB: This is a "you should not do" example.

Github code

The good: First thing to notice is, that nested Proxies works perfectly fine together and call order is respected; The first two log targets are proxy and the last one is the original target. (This will be important for the next step)

I have also simulated a "long" running process, so you could imagine API calls that might influence the behavior of your Proxy.

The bad: Second thing to notice, the target object behavior has been modified externally; the original object property can no longer be accessed directly due to the chain of Promises. Now the data is accessible through Promise.prototype.then() or Async/Await, otherwise only got the log inside the chain of proxies but I'm not awaiting the resolve/reject value.

The ugly: The different handlers here are almost identical, don't repeat yourself, and take some time to refactor your code.

Profiling + Logging with obfuscation example

Profiling is used to measure elapsed time to do something. Logging is used to keep a trace of what happens in your App.

Github code

Some explanation about this example:

I use uuidv4 to have consistent traces in my profiling console logs. Some might prefer to save a timestamp and compute the differences manually to avoid the usage of "UUID" in the logs which reduces readability.

I do not recommend doing this on production except if you reduce the scope of your profiling marks to the strict minimum and use a proven log drain instead of the web browser console.

Your frontend should also add a request ID (generated for a single user action); (and a session ID if you have an authenticated section in your App) in your log templates to ensure better traceability across distributed tracing systems (to tail logs across multiple microservices).

For obfuscation, you could use a collection (a Set here) to hide sensitive data. For example, you could filter props like "creditCard"…

Some might prefer to use a matching pattern approach (RegExp) based on a naming strategy, like if a prop starts with "get" or if a prop contains "something".

Both are based on the "prop" prop of the get and set of your Proxy handler.

get(target, prop, receiver)
set(target, prop, value, receiver)

In order to consume these handler, we can process like this.

Github code

Memoïzation example

Memoïzation is an optimization technique where expensive function calls are cached such that the result can be immediately returned the next time the function is called with the same arguments.

-Jonathan Lehman

It's designed to be used with pure function (depending only on its parameter, not based on external state). So it has to be explicit in your Proxy description.

Memoïzation might seem easier to use with Decorators because there a less abstraction between its design and the target on which it's going to be applied.

Github code

Here is a realy basic implementation of Memoïzation, but you should also add a size limitation to your collection to avoid memory issues.

When you know that a data is processed inside your pipeline that is not pure (dependent on a state), you might still need some cache on your frontend but you'll need to define a cache duration validity and store calls to measure elapsed time since the last call, if greater than X seconds/minutes, then re-compute the prop value…

Memoïzation illustration

Credit to "Dan Bejar/The iSpot"

Perf & limitations

Pref : Proxies open many possibilities but these are very costly, depending on their implementation, compared to accessing a property of an Object (~86x to ~175x more expensive).

Tableau de benchmark comparant les performances en matière de « property access »

Credit to Adrien Maret for the benchmark

This cost does not prohibit the use of Proxies, it's all about context.

Limitation:

  • Target : Many built-in objects with "internal slots" such as Map / Set / Date … or even DOM nodes, does not work natively with Proxies; Try for example to add a value in a Map through a Proxy, you will get an error;
  • Private: For a class containing private fields, these are by nature not intended to be exposed and proxies do not allow access to these properties.

(Unless you use the Reflection in a Handler by contextualizing the manipulated type)

Conclusions

AOP is a great tool; You should use it carefully and prevent overuse. Proxies are versatile, thanks to the target and the Reflect API; Nesting could be tricky in some cases and don't forget the trade-offs on perf. Great to use on Object/Class/Array instances, for your own code or external libraries.

Next paper, we will explore similar concepts with Decorator through more advanced examples.

You should explore this by yourself; like a donjon crawler; Die and retry, this is a way to forge your knowledge; And then share your experiences with your pairs.