Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 57 di 112
  • livello avanzato
Indice lezioni

Decorator

Il design pattern dei Decorator è diventato sempre più diffuso su Javascript grazie all'avvento di Angular ed altri framework. Ecco come utilizzarlo.
Il design pattern dei Decorator è diventato sempre più diffuso su Javascript grazie all'avvento di Angular ed altri framework. Ecco come utilizzarlo.
Link copiato negli appunti

Una delle caratteristiche più interessanti del moderno JavaScript è rappresentato dai decorator. In realtà, al momento della stesura di questo articolo, la funzionalità non è ancora ufficialmente standardizzata: essa si trova infatti nella fase 2 del processo di standardizzazione e pertanto può essere soggetta a modifiche. Tuttavia i decorator sono già utilizzabili grazie al supporto da parte di alcuni transpiler, come ad esempio Babel
e TypeScript, e sono diventati popolari soprattutto in seguito all'uscita di Angular 2.

Ma cosa sono in pratica i decorator, e perché sono così interessanti?

Decorator Design Pattern

Da un punto di vista concettuale, i decorator non sono una novità assoluta. Essi non sono altro che un design pattern in base al quale una porzione di codice viene arricchita con altro codice prima e/o dopo. Per spiegarci meglio, consideriamo la seguente funzione:

function logDecorator(f) {
   return function() {
      console.log("Inizio esecuzione");
      var result = f.apply(this, arguments);
      console.log("Fine esecuzione");
      return result;
   };
}

Essa prende in input una funzione e restituisce una nuova funzione che esegue quella in input arricchendola con la scrittura di messaggi sulla console. La funzione logDecorator() è un decorator.

Consideriamo ora la seguente funzione che si limita a sommare due numeri:

function somma(x, y) {
   return x + y;
}

Possiamo combinarla con il decorator definito prima nel seguente modo:

var sommaConLog = logDecorator(somma);

Con questo assegnamento otteniamo la funzione somma() arricchita dalla scrittura sulla console. Quindi potremo utilizzarla nel seguente modo:

console.log(sommaConLog(2, 3));
//Inizio esecuzione
//Fine esecuzione
//5

Il principale vantaggio dell'applicazione di questo pattern consiste nel separare la logica di più attività componendo più funzioni. Nel nostro esempio abbiamo separato la logica della somma di due numeri da quella della scrittura di un log sulla console, semplificando la comprensione del codice e di conseguenza la manutenibilità.

Decorator in ECMAScript

Le specifiche ECMAScript semplificano questo pattern offrendo una sintassi dichiarativa molto comoda. In pratica un decorator è una qualsiasi espressione che definisce una funzione preceduta dal simbolo @ e può essere applicata ad una classe o ai suoi membri. Consideriamo ad esempio la seguente classe:

class MyMath {
    @log
    somma(x, y) {
       return x + y;
    }
 }

Il decorator @log indica che vorremmo applicare la funzione log() ogni volta che viene invocato il metodo. La funzione da eseguire potrebbe essere la seguente:

function log(target) {
    console.log("Inizio esecuzione");
    var result = target.apply(this, arguments);
    console.log("Fine esecuzione");
    return result;
 }

Notiamo che alla funzione log() che implementa il nostro decorator viene passato dal sistema il parametro target che rappresenta il metodo su cui è applicato il decorator stesso. Quindi questa implementazione è del tutto analoga a quella precedente, per cui quando invocheremo il metodo somma() otterremo lo stesso risultato dell'applicazione del pattern decorator visto prima:

var math = new MyMath();
math.somma(2, 3);
//Inizio esecuzione
//Fine esecuzione
//5

Le specifiche ECMAScript prevedono però un passaggio di parametri più ricco del semplice membro della classe a cui applicare il decorator. Infatti, ad un decorator vengono passati tre parametri:

  • target: rappresenta, come abbiamo visto, il membro su cui viene applicato il decorator
  • name: è il nome del decorator
  • descriptor: è il descrittore del membro; in pratica lo stesso oggetto utilizzato per definire una proprietà con Object.defineProperty()

La possibilità di avere altre informazioni sul membro che si sta decorando è molto utile in quanto ci consente di avere un
controllo più granulare e di implementare funzionalità interessanti. Ad esempio, se volessimo implementare un decorator che trasformi un membro di una classe in sola lettura potremmo procedere nel seguente modo:

function readonly(target, name, descriptor) {
    descriptor.writable = false;
    return descriptor;
 }

In questo caso abbiamo sfruttato la presenza del descrittore per modificare le sue impostazioni.

Da notare che abbiamo restituito il descrittore stesso, buona pratica da mantenere per supportare correttamente l'applicazione di più decorator sullo stesso membro.

Possiamo quindi utilizzare il decorator readonly nel seguente modo:

class MyMath {
    @readonly, @log
    somma(x, y) {
       return x + y;
    }
 }

In questo modo escludiamo l'eventualità che possa assere riassegnato il valore del membro somma con un'altra funzione o un altro valore, cioè che possa essere stravolto il significato della nostra classe MyMath.

Segnaliamo che in presenza di più decorator su un membro di una classe, essi vengono applicati in base all'ordine dichiarato.

Decorator factory

In certi casi potremmo voler avere comportamenti leggermente diversi di un decorator in base a dei criteri decisi da chi usa il decorator stesso. Proviamo a chiarire con un esempio.

Supponiamo che per il nostro metodo somma() vogliamo assicurarci della validità del tipo dei parametri, cioè vogliamo che al metodo vengano passati esclusivamente valori numerici.

Naturalmente la prima cosa che ci viene in mente è di trasformare il metodo nel seguente modo:

class MyMath {
    somma(x, y) {
       let result;
       if (typeof x === "number" && typeof y === "number") {
          result = x + y;
       } else {
          throw new Error("Il tipo dei parametri deve essere numerico");
       }
       return result;
    }
 }

L'aggiunta del controllo sul tipo dei parametri porta un po' a stravolgere il semplice compito che aveva il metodo: quello di sommare due numeri.

Per semplificare la comprensibilità del codice possiamo delegare ad un decorator il compito di verificare il tipo di dato dei parametri passati al metodo. Immaginiamo qualcosa come quello mostrato di seguito:

class MyMath {
    @validate([{
       type: "number"
    }, {
       type: "number"
    }])
    somma(x, y) {
       return x + y;
    }
 }

Nell'esempio abbiamo immaginato un decorator validate che prende in input un array di criteri di validazione. Ciascun elemento dell'array corrisponde nell'ordine a ciascun parametro previsto dal metodo somma(). Ciascun criterio di validazione è rappresentato da un oggetto con la proprietà type che esprime il tipo previsto per il relativo parametro.

La nostra idea è che il decorator si occupi di verificare se ciascun parametro passato al metodo somma() sia del tipo previsto. Ci troviamo tuttavia di fronte ad una interessante novità rispetto agli esempi di decorator visti finora: questo decorator ha dei parametri. Da quel che sappiamo finora, ad un decorator vengono soltanto passate le tre informazioni relative al membro a cui è associato. Come facciamo a gestire questi nuovi parametri?

La soluzione consiste nel ricorrere ad un decorator factory, cioè una funzione che restituisce un decorator. Infatti, se ricordiamo la definizione di decorator, abbiamo detto che esso è una qualsiasi espressione che restituisce una funzione. Quindi non ci resta che provare a scrivere una funzione che prende in input i criteri di validazione e restituisce una nuova funzione che li applica ai parametri passati al metodo somma(). Il seguente codice potrebbe fare al caso nostro:

function validate(criteriValidazione)
   return function decorator(target, name, descriptor) {
      for (let i = 0; i < arguments.length; i++) {
         if (typeof arguments[i] !== criteriValidazione[i].type) throw new Error("Tipo di parametro errato");
      }
      descriptor.value = function() {
         return target.apply(this, arguments);
      };
      return descriptor
   }
}

Notiamo la presenza del ciclo for che ci assicura che tutti i parametri passati siano del tipo previsto. Notiamo anche come il resto del codice non esegua direttamente il target, come facevamo negli esempi precedenti, ma modifichi il descrittore in modo che il nostro decorator possa essere composto con eventuali altri decorator.

In questo modo abbiamo mantenuto la semplcità del codice del metodo somma() e creato un decorator che può essere riutilizzato in altri contesti analoghi.


Ti consigliamo anche