La classe Proxy, introdotta con le specifiche ECMAScript 2015, consente
di creare oggetti che hanno la capacità di modificare il
comportamento predefinito di altri oggetti. Nella definizione di un
proxy per un oggetto, cioè di un'istanza della classe Proxy,
possiamo definire un handler e configurare trap per
intercettare l'accesso alle sue proprietà ed eventualmente
modificare il comportamento predefinito.
Per comprendere i concetti di base di un proxy, proviamo a fare un
semplice esempio. Supponiamo di voler tracciare sulla console ogni
accesso alle proprietà di un oggetto. Possiamo definire il seguente handler:
var handler = {
get(target, propertyName) {
console.log("Lettura di " + propertyName);
return target[propertyName];
},
set(target, propertyName, value) {
console.log("Assegnamento di " + value + " a " + propertyName);
target[propertyName] = value;
}
};
Questo handler non è altro che un oggetto con due metodi, get()
e set()
, che intercettano rispettivamente gli accessi in lettura e scrittura alle proprietà dell'oggetto che vogliamo
monitorare. I metodi dell'handler sono chiamati trap e
consentono di intercettare accessi e manipolazioni relative all'oggetto di
destinazione, il target.
Nello specifico, il metodo get()
scrive sulla console e
restituisce il valore della proprietà del target, mentre il metodo set()
scrive il valore sulla console ed assegna il valore del
parametro value alla proprietà del target. In questo caso vogliamo
mantenere il comportamento standard dell'oggetto target, ma in
generale possiamo restituire o assegnare alla proprietà del target
qualsiasi valore, modificando quindi il comportamento predefinito.
Una volta definito l'handler, possiamo creare un proxy per un
oggetto specificandolo nel costruttore della classe Proxy, come nel
seguente esempio:
var persona = {nome: "Mario", cognome: "Rossi"};
var personaProxata = new Proxy(persona, handler);
Abbiamo creato un oggetto persona e lo abbiamo passato insieme all'handler al costruttore della classe Proxy. D'ora in poi ogni accesso alle proprietà dell'oggetto personaProxata avrà effetto sull'oggetto persona e verrà intercettato e loggato sulla console:
var nome = personaProxata.nome;
//console: Lettura di nome
personaProxata.nome = "Marco";
//console: Assegnamento di Marco a nome
console.log(persona.nome);
//console: Marco
Naturalmente
questo è un semplice esempio per introdurre i concetti di base
per l'utilizzo della classe Proxy. È possibile utilizzare
altre trap per definire manipolazioni avanzate dell'oggetto
target. Oltre a get()
e set()
, infatti, possiamo
sfruttare le seguenti trap che intercettano i corrispondenti
metodi dell'oggetto target:
-
getPrototypeOf()
-
setPrototypeOf()
-
isExtensible()
-
preventExtensions()
-
getOwnPropertyDescriptor()
-
defineProperty()
-
has()
-
deleteProperty()
-
ownKeys()
-
apply()
-
construct()
Validazione con i proxy
Abbiamo visto i meccanismi di base dell'utilizzo della classe Proxy con un esempio molto semplice e didattico. Nella pratica i
contesti di applicazione sono diversi e possono contribuire a scrivere
codice meno invasivo e con una elevata separazione delle responsabilità.
Proviamo a dare qualche indicazione fornendo qualche esempio più pratico.
Supponiamo di voler gestire la validazione dell'assegnamento di valori ad
un oggetto senza modificare però l'oggetto in questione. Ad esempio,
vogliamo che non sia possibile assegnare una stringa vuota o una stringa
che contenga dei numeri al nome e cognome di un oggetto che rappresenti una
persona. Possiamo allora definire un handler come il seguente:
var validatore = {
set(target, propertyName, value) {
if (propertyName == "nome" || propertyName == "cognome") {
if (value == "") throw new Error("Non è possibile assegnare una stringa vuota");
let hasNumberRegExp = /\d/;
if (hasNumberRegExp.test(value)) throw new Error("La stringa non può contenere numeri");
}
target[propertyName] = value;
}
};
Come possiamo vedere, abbiamo definito il metodo set()
all'interno
del quale abbiamo intercettato gli accessi alle proprietà nome e cognome. Nel caso in cui il valore che si tenta di assegnare a
queste proprietà è una stringa vuota o contiene un valore numerico, generiamo
un'eccezione con uno specifico messaggio. Se non si intercetta una
situazione non valida, viene effettuato l'assegnamento sulla proprietà
dell'oggetto target.
Vediamo come implementare e testare il proxy:
var personaConValidazione = new Proxy({nome: "Mario", cognome: "Rossi"}, validatore);
personaConValidazione.nome = "";
//Uncaught Error: Non è possibile assegnare una stringa vuota
personaConValidazione.nome = "Mario1"
//Uncaught Error: La stringa non può contenere numeri
In questo modo abbiamo isolato la logica di validazione in un componente
esterno all'oggetto originario migliorando la manutenibilità del codice. Ad
esempio, se i criteri di validazione per un oggetto cambiano da un contesto
all'altro, non abbiamo bisogno di scrivere oggetti diversi o setter
complessi per applicare il criterio corretto. È sufficiente scrivere
handler diversi in base al contesto ed utilizzarli per creare il proxy opportuno.
Data binding
Un altro ambito in cui possiamo utilizzare la classe Proxy è
nell'implementazione del data binding, cioè nel meccanismo che
lega le proprietà di due oggetti in modo che le modifiche si propaghino da
uno all'altro. Nel contesto del data binding si parla di un
oggetto che fornisce dati (data source object) e di un oggetto che
li riceve (data target object). L'esempio tipico di applicazione
del data binding è quello che associa una proprietà di un oggetto
con un elemento dell'interfaccia grafica, come ad esempio una casella di
testo. Vediamo come sfruttare la classe Proxy per implementare il
meccanismo di data binding.
Definiamo una classe Binder con un metodo bindTo()
come
mostrato di seguito:
class Binder {
bindTo(dataSourceObj, dataSourceProperty, dataTargetObj, dataTargetProperty) {
var bindHandler = {
set: function(target, property, newValue) {
if (property == dataSourceProperty) {
target[dataSourceProperty] = newValue;
dataTargetObj[dataTargetProperty] = newValue;
}
}
};
return new Proxy(dataSourceObj, bindHandler);
}
}
Il metodo
bindTo()
definisce una trap che cattura gli accessi in
scrittura al data source object, in modo tale che ogni
modifica alla proprietà specificata dal parametro
dataSourceProperty aggiorni la proprietà associata del
data target object. Il metodo bindTo()
restituisce il
proxy creato a partire dal data source object. Possiamo quindi
usare la classe Binder come nel seguente esempio:
var persona = {
nome: "Mario",
cognome: "Rossi"
};
var txtNome = document.getElementById("txtNome");
var binder = new Binder();
var personaConBinding = binder.bindTo(persona, "nome", txtNome, "value");
setTimeout(function() {
personaConBinding.nome = "Marco";
}, 5000);
Abbiamo creato il proxy dell'oggetto persona utilizzando il metodo bindTo()
della classe Binder. Nella chiamata al metodo bindTo()
abbiamo specificato che vogliamo mettere in relazione la
proprietà nome dell'oggetto persona con la proprietà value dell'elemento txtNome. In questo modo, ogni modifica
alla proprietà nome dell'oggetto personaConBinding si
rifletterà automaticamente sia sull'oggetto originario persona,
sia sulla casella di testo.
I due esempi di applicazione concreta che abbiamo riportato danno un'idea
delle possibili utilizzi della classe Proxy nello sviluppo di
applicazioni di una certa complessità.