Ecmascript 6 ha introdotto già da qualche tempo una API molto interessante, forse sottovalutata: l'oggetto Proxy. Tramite esso è possibile modificare il comportamento standard delle classi Javascript, andando a decorare o ridefinire il comportamento dei metodi.
Per capire meglio il concetto di Proxy è necessario introdurre, come una sorta di glossario, 3 concetti chiave che utilizzeremo successivamente:
- target: l'oggetto obiettivo del proxy, il cui comportamento viene di fatto modificato
- handler: l'oggetto che incapsula le modifiche da applicare al target
- traps: il metodo che fornisce accesso alla proprietà e che implementa di fatto la modifica al comportamento
Sintassi
L'utilizzo della classe Proxy è abbastanza semplice. L'oggetto si istanzia passando al costruttore due parametri: il target e l'handler:
var proxy = new Proxy(target, handler);
Utilizzo
Una volta introdotti i concetti chiave, possiamo partire con un esempio semplice:
var person = {
name: 'Alberto'
};
var handler = {
get: function(target, prop) {
console.log('Someone is trying to read ' + prop);
return target[prop];
}
}
var p = new Proxy(person, handler);
console.log(p.name);
In questo esempio abbiamo creato una classe target (l'oggetto person
) che ha una proprietà name
e un handler che implementa come trap il metodo get
. Questo metodo viene invocato ogni volta che qualcuno cerca di accedere ad una proprietà dell'oggetto target. In questo esempio abbiamo introdotto una operazione di log che notifica che qualcuno sta appunto cercando di accedere alla proprietà.
L'esecuzione di questo codice genererà due messaggi in output, prima la notifica del tentativo di accesso e poi il valore "Alberto".
Trap
Come prevedibile, i trap sono in numero finito e ognuno rappresenta una modalità di interazione con un oggetto. Definire il comportamento del trap significa in qualche modo inserirsi nel normale flusso di esecuzione della modalità di interazione. Vediamo quali sono i trap messi a disposizone dall'API:
-
handler.getPrototypeOf(target): permette di ascoltare il metodo
Object.getPrototypeOf
invocato sull'oggetto target -
handler.setPrototypeOf(target, prototype): permette di ascoltare il metodo
Object.setPrototypeOf
invocato sull'oggetto target -
handler.isExtendible(target): permette di ascoltare il metodo
Object.isExtendible
invocato sull'oggetto target -
handler.preventExtensions(target): permette di ascoltare il metodo
Object.preventExtensions
invocato sull'oggetto target -
handler.getOwnPropertyDescriptor(target, prop): permette di ascoltare il metodo
Object.getOwnPropertyDescriptor
invocato sull'oggetto target -
handler.defineProperty(target, key, descriptor): permette di ascoltare il metodo
Object.defineProperty
invocato sull'oggetto target -
handler.has(target, key): permette di ascoltare il metodo
in
dell'oggetto target - handler.get(target, prop, receiver); permette di ascoltare eventuali accessi in lettura alle proprietà dell'oggetto target
- handler.set(target, prop, value): permette di ascoltare eventuali accessi in scrittura alle proprietà dell'oggetto target
-
handler.deleteProperty(target, prop): permette di ascoltare l'invocazione dell'operatore
delete
sull'oggetto target -
handler.ownKeys(target): permette di ascoltare i metodo
Object.getOwnPropertyNames
eObject.getOwnPropertySymbols
invocati sull'oggetto target - handler.apply(target, scope, argumentsList): permette di ascoltare un'eventuale invocazione dell'oggetto
- handler.construct(target, argumentsList): permette di ascoltare l'invocazione del costruttore
Esempi approfonditi: validazione
Una delle possibilità più sfruttate per l'utilizzo dei Proxy è la validazione, ovvero la possibilità di lanciare un'eccezione quando si cerca di impostare un valore errato ad una property dell'oggetto target:
var handler = {
set: function(target, prop, value) {
if(prop === "name") {
this.validateName(value);
} else if(prop === "age") {
this.validateAge(value);
} else {
throw new Error("Property " + prop + " does not exists");
}
target[prop] = value;
},
validateAge: function(age) {
if(typeof age !== "number") {
throw new Error("Age must be a number");
}
},
validateName: function(name) {
if(typeof name !== "string") {
throw new Error("Name must be a string");
}
}
}
var person = new Proxy({}, handler);
In questo esempio abbiamo implementato il trap set introducendo un layer di validazione, basato sul nome della proprietà che si cerca di modificare.
Un'invocazione di person.name = 'Alberto'
non genererà nessun errore, mentre person.age = '25'
lancierà l'eccezione "Age must be a number".
Si noti che è possibile anche creare un oggetto Proxy avente come target un oggetto vuoto.
Per approfondire la validazione tramite Proxy, consigliamo la libreria proxy-validator, che offre una API abbastanza ricca per la validazione dei nostri oggetti.
Esempi approfonditi: accesso ad API
Un altro esempio molto interessante è quello legato all'accesso ad API remote. Tramite i Proxy possiamo implementare un layer di accesso ai dati dinamico e al tempo stesso comodo e eloquente.
Nell'esempio seguente, l'accesso al servizio remoto è solamente simulato per non spostare il focus dai Proxy:
var handler = {
get: function(target, prop) {
if(!prop.startsWith('get')) {
return;
}
var url = "http://api/" + prop.substring(3)
return function(params) {
console.log("Simulate remote service...", url, params);
}
}
}
var p = new Proxy({}, handler);
p.getPeople(); // perform a GET http://api/People
p.getPersonById(1); // perform a GET http://api/PersonById
In questo caso abbiamo riutilizzato il trap get implementando un controllo più approfondito per separare il nome del metodo HTTP (in questo esempio è implementato solamente il metodo GET) dal nome del servizio remoto. Una volta identificati i due segmenti possiamo finalmente ritornare una funzione che riceverà eventuali parametri che verranno poi inviati al server all'endpoint specificato prima.
Compatibilità
Fortunatamente l'adozione dell'oggetto Proxy è abbastanza avanzata. L'API è praticamente disponibile su tutti i principali browser moderni sia desktop che mobile, e sulle ultime versioni di NodeJS per utilizzarlo anche in ambito server. La tabella di compatibilità è disponibile a questo link.
Conclusioni
Nonostante l'utilizzo della classe Proxy sia abbastanza triviale, questa API offre davvero una miriade di possibilità, l'unico limite è la creatività dello sviluppatore che può ulteriormente migliorare le proprie librerie e implementare con facilità dei design pattern fino ad ora impossibili, come ad esempio la Aspect Oriented Programming (ricordate che un proxy può diventare a suo volta target per un altro handler).
La comunità Javascript ha già da tempo fatta propria questa tecnologia e sta già sfruttando questa nuova possibilità per creare librerie di diverso tipo che vanno a facilitare e a rendere più divertente sviluppare applicativi. Non resta quindi che scoprire le migliori librerie, studiarle e utilizzarle.