JavaScript Proxies

Qualche tempo fa abbiamo ospitato un ottimo post sulle novità di EcmaScript 6. Ovviamente lo scopo di quel post era introduttivo e non ha esplorato tutte le nuove funzionalità messe a disposizione dal nuovo standard. Tra queste una delle più interessanti è l’introduzione dei Proxies. Il Proxy è uno dei design pattern classici definiti dalla Gang of Four nel loro libro “Design Patterns”. Un Proxy è un oggetto che funziona da “interfaccia” verso l’esterno di un altro oggetto. Lo schema UML del pattern è il seguente:

Proxy Pattern

Proxy Pattern – Wikimedia Commons

Gli scopi di un Proxy sono molteplici e dipendono molto dal contesto. Piuttosto che fare una carrellata teorica di possibili scenari, vedremo dei casi pratici di utilizzo.

Compatiblità

Prima di buttarci a capofitto negli esempi facciamo un attimo il punto sulla compatibilità di questa nuova feature. La situazione attuale (Dicembre 2016) che potete controllare sulla pagina dedicata di caniuse.com è la seguente:

Compatibilità Proxy Object

Compatibilità Proxy Object

Come vedete non è possibile utilizzarlo in produzione “as is”. Si può però utilizzare questo polyfill parziale del team di Google Chrome, oppure questo plugin per Babel se utilizzate già il noto transpiler.

Logging

Nel nostro primo incontro con i Proxies ci occuperemo di Logging. Il caso d’uso è il seguente: immaginate di avere un oggetto che non ha nessuna business logic ma semplicemente proprietà. Vogliamo ora loggare tutti gli accessi a questo oggetto, in pratica vogliamo scrivere in console ogni volta che una proprietà viene letta tramite getter o scritta tramite setter.

const INITIAL_DATA = {
    name: 'Francesco',
    surname: ''
};

const loggable = (target) => {
    const loggingHandler = {
        get: function (target,name) {
            const value = target[name];
            console.log(`getting ${name}: ${value}`);
            return value;
        },
        set: function (target,name,value) {
            console.log(`setting ${name}: ${value}`);
            target[name] = value;
            return true;
        }
    };

    return new Proxy(target,loggingHandler);
};

const person = loggable(INITIAL_DATA);

const name = person.name;  //log 'getting name: Francesco'
person.surname = 'Strazzullo';  //log 'setting surname: Strazzullo'

Come vedete per creare un Proxy si wrappa un oggetto iniziale con una serie di handlers. In questo caso utilizziamo per l’appunto get e set che vengono rispettivamente invocati alla lettura o alla scrittura di ogni singola proprietà dell’oggetto. Notate inoltre che per il resto dell’applicazione l’oggetto non cambia il suo comportamento, quindi questa tecnica è ottima per modificare una base di codice legacy.

Observable

Passiamo ad un esempio leggermente più utile, invece che limitarci a loggare i cambiamenti di stato di un oggetto qualsiasi, vogliamo essere notificati di questi cambiamenti per poter fare delle operazioni. In pratica vogliamo creare in maniera rapida degli Observable a partire da qualsiasi oggetto JavaScript.

const INITIAL_DATA = {
    name: 'Francesco',
    surname: ''
};

const observable = ({target,listener}) => {
    let observable;

    const set = (target,name,value) => {
        target[name] = value;
        listener(observable);
        return true;
    };

    const handler = {
        set
    };

    observable = new Proxy(target,handler);

    return observable;
};

const listener = (person) => {
    console.log('person changed');
}

const person = observable({
    target: INITIAL_DATA,
    listener
});

person.surname = 'Strazzullo';  //log 'person changed'

Come potete aver notato con questa tecnica potete quindi creare Observable in maniera trasparente per il resto dell’applicazione. In pratica questo può essere uno step iniziale per rendere reattiva la vostra applicazione, senza doversi incamerare il debito tecnico di una libreria complessa come RxJS.

Meme

Reactive Programming === Observe All The Things!

Validazione

Come ultimo esempio creeremo un piccolo sistema di validazione per i nostri oggetti JavaScript. Per ora ci limiteremo ad un piccolo check sul tipo.

const typeOfValidatorFactory = (expectation) => {
    return (value, name) => {

        //undefined values should be considered 'good'
        if(value === undefined){
            return
        }

        if (typeof value !== expectation) {
            throw new Error(`${value.constructor.name} is not a valid for '${name}' property. Should be ${expectation}`);
        }
    }
};

const validators = {
    'String': typeOfValidatorFactory('string'),
    'Number': typeOfValidatorFactory('number')
};

const checked = ({target, definition}) => {
    let proxy;

    const set = (target, name, value) => {
        const currentDefinition = definition[name];

        if (!currentDefinition) {
            throw new Error(`${name} is not a valid property for this object`);
        }

        const validator = validators[currentDefinition.name] || (() => {});

        validator(value, name);

        target[name] = value;

        return true;
    };

    const handler = {
        set
    };

    proxy = new Proxy(target, handler);

    return proxy;
};

const DEFINITION = {
    name: String,
    surname: String,
    age: Number
};

const INITITAL_DATA = {
    name: 'Francesco'
};

const person = checked({
    target:INITITAL_DATA,
    definition:DEFINITION
});

person.surname = 'strazzullo'; //ok
person.age = 30; //ok
person.age = '30' //should throw

In questo caso abbiamo un validazione davvero semplicistica, possiamo solo definire se la proprietà di un oggetto è una String oppure un Number. Ma partendo da questo esempio, con poche righe di codice, si può creare un engine di validazione davvero complesso. Potremmo infatti collegare regole di business ai nostri oggetti oppure creare un piccolo type system a runtime per JavaScript.

Conclusioni

Come avete potuto vedere i JavaScript Proxies sono davvero uno strumento potente. Permettono con poche righe di creare architetture davvero complesse. Soprattutto per la loro struttura sono facilmente integrabili con qualsiasi framework e con una codebase già esistente. Per chi volesse approfondire ho anche scritto un post su Medium su come creare tramite i Proxies e il Virtual Dom un piccolo framework reattivo. Alla prossima.

Francesco Strazzullo

Faccio il Front-end engineer per extrategy dove mi occupo di applicazioni Angular, React e applicazioni mobile. Da sempre sono appassionato di Architetture software e cerco di applicarle in maniera innovativa allo sviluppo frontend in generale.