Vai al contenuto

Design pattern in Javascript: 4 modelli interessanti

Ogni sviluppatore si dovrebbe sforzare a scrivere codice mantenibile, leggibile, e riutilizzabile. Il codice acquista importanza in maniera direttamente proporzionale alle dimensioni dell’applicazione. L’uso dei design pattern si rivela cruciale per risolvere questo problema in quanto forniscono una struttura orgainzzata per i problemi più comuni nel contesto dei linguaggi di programmazione.

Durante le varie fasi di creazione di un’applicazione, gli sviluppatori interagiscono spesso con modelli di progettazione, anche in modo inconsapevole. Anche se esistono diversi modelli di progettazione, gli sviluppatori Javascript tendono ad utilizzare alcuni modelli abitualmente più di altri. Questi modelli sono i seguenti:

  • Module
  • Prototype
  • Observer
  • Singleton

Ogni modello è costituito da molte proprietà, però, io sottolineare i seguenti punti chiave:

  1. Contesto : Dove / in quali circostanze posso utilizzare il modello?
  2. Problema : cosa stiamo cercando di risolvere?
  3. Soluzione : Come utilizzare questo modello per risolvere il nostro problema?
  4. Implementazione : A cosa assomiglia l’implementazione?

Design pattern Module

I moduli sono i design pattern più usati per mantenere particolari pezzi di codice indipendente da altri componenti. Questo fornisce un accoppiamento debole per supportare un codice ben strutturato.

Per coloro che hanno familiarità con linguaggi orientati agli oggetti, i moduli sono “classi” JavaScript. Uno dei molti vantaggi dell’utilizzo delle classi è il cosidetto incapsulamento: stati e comportamenti della classe vengono incapsulati all’interno della classe controllando l’accesso agli stessi da altre classi esterne. Il design pattern module permette vari livelli di accesso: public, private, protected e privileged.

I moduli vanno scritti seguendo la metodologia IIFE (Immediately-invoked function expression) cioè, utilizzando una “chiusura” che protegge variabili e metodi (tuttavia, verrà restituito un oggetto invece di una funzione). In pratica:

(function() {

    // qui si dichiarano le variabili e le funzioni private

    return {
      // qui si dichiarano le variabili e le funzioni publiche
    }

})();

Per fare un esempio più concreto

var HTMLEditor = (function() {
  var contenuto = 'testo di prova';

  var cambiaHTML = function() {
    var elem = document.getElementById('id-elemento');
    elem.innerHTML = contenuto;
  }

  return {
    CambiaHTML_pubblico: function() {
      cambiaHTML();
      console.log(contenuto);
    }
  };

})();

HTMLEditor.CambiaHTML_pubblico();
// Outputs in console: 'testo di prova'

console.log(HTMLChanger.contenuto);  
// undefined (si cerca di accedere a una variabile privata)

In parole povere, in HTMLEditor  abbiamo creato un oggetto che contiene:

  • funzioni pubblici: CambiaHTML_pubblico()
  • funzioni e variabili privati: cambiaHTML()contenuto

La funzione CambiaHTML_pubblico() sarà richiamabile anche senza l’utilizzo del namespace HTMLEditor, mentre provando ad accedere alla variabile contenuto, verrà prodotto un errore undefined.

Revealing Module Pattern

E’ una variazione del pattern Module. Lo scopo è quello di mantenere tutto incapsulato in forma privata e “rivelare” alcune variabili e alcuni metodi restituiti in un oggetto letterale. L’implementazione diretta si presenta così:

var Test = (function() {
  var privateVariable = 10;

  var funzionePrivata = function() {
    console.log('Funzione privata');
    privateVariable++;
  }

  var funzionePubblica = function() {
    console.log('Funzione pubblica');
  }

  var altraFunzionePubblica = function() {
    funzionePrivata();
  }

  return {
      primo: funzionePubblica,
      secondo: altraFunzionePubblica 
  };
})();

Test.primo();        // Output: Funzione pubblica
Test.secondo();       // Output: Funzione privata
Test.funzionePubblica; // undefined

Anche se quest’ultimo sembra molto più pulito, uno svantaggio evidente è quello che siamo in grado di far riferimento ai metodi privati. Questo può comportare problemi con gli unit test. Inoltre, i behaviors pubblici non sono overridable.

Design Pattern Prototype

Il design pattern Prototype si basa sulla eredità del Prototype JavaScript. Questo modello si usa solitamente quando vi è un carico computazionale di una certa rilevanza per migliorare le performance. Prototype permette di creare nuovi oggetti clonando un oggetto iniziale già costruito, detto appunto prototipo. A differenza di altri pattern come Abstract factory o Factory method permette di specificare nuovi oggetti a tempo d’esecuzione (run-time), utilizzando un gestore di prototipi (prototype manager) per salvare e reperire dinamicamente le istanze degli oggetti desiderati. Un caso d’uso classico è l’esecuzione di una query su un database per creare un oggetto utilizzato in alcune parti dell’applicazione. Se un altro processo deve utilizzare lo stesso oggetto, anzichè ricrearlo, potrebbe risultare vantaggioso clonare l’oggetto esistente creato in precedenza.

Per clonare un oggetto, deve esistere un costruttore che istanzi l’oggetto prototipo iniziale. Successivamente, utilizzando la parola chiave prototype, le variabili e i metodi possono essre “legati” (bind) all’oggetto in modo da estenderlo. Diamo un’occhiata a un esempio:

var Automobile = function() {
  this.numRuote = 4;
  this.marca = 'Fiat';
  this.modello = 'Punto';
}

Automobile.prototype.accelera = function() {
  // le ruote girano
}

Automobile.prototype.frena = function() {
  // le ruote si bloccano
}

Il costruttore crea il singolo oggetto Automobile. La creazione di nuovi oggetti di tipo Automobile, manterrà gli stati inizializzati nel costruttore. Inoltre, le funzioni accelera e frena verranno mantenute anche negli altri oggetti, poichè son ostati dichiarati attraverso prototype. L’esempio sopra possiamo scriverlo in quest’altra maniera:

var Automobile = function() {
  this.numRuote = 4;
  this.marca = 'Fiat';
  this.modello = 'Punto';
}

Automobile.prototype = {
  accelera: function() {
    // le ruote girano
  },
  frena: function() {
    // le ruote si bloccano
  }
}

Revealing Prototype Pattern

Come già visto per il pattern Module, anche per il Prototype esiste una variante. Questo pattern fornisce un incapsulamento dei metodi accelera(), frena() e delle variabili e ritorna un oggetto letterale:

var Automobile = function() {
  this.numRuote = 4;
  this.marca = 'Fiat';
  this.modello = 'Punto';
}

Automobile.prototype = function() {
  var accelera = function() {
    console.log("le ruote girano");
  };

  var frena = function() {
    console.log("le ruote si bloccano");
  };

  return {
    premiPedaleFreno: frena,
    premiPedaleAcceleratore: accelera
  }
}();

auto = new Automobile();
auto.premiPedaleFreno();  // le ruote si bloccano
auto.accelera();  // genera errore

Design Pattern Observer

Molte volte, quando una parte dell’applicazione cambia, le altre parti devono aggiornarsi di conseguenza.  In AngularJS, ad esempio, se l’oggetto $scope si aggiorna, può essere catturato un evento che notifichi l’aggiornamento agli altri componenti.

È un pattern intuitivamente utilizzato come base architetturale di molti sistemi di gestione di eventi. Sostanzialmente il pattern si basa su uno o più oggetti, chiamati Osservatori o Observer, che vengono registrati per ascoltare un evento generato dall’oggetto “osservato”, che chiamiamo Soggetto (Subject). Oltre all’observer è necessario il Concrete Observer che si differenzia dal primo perché implementa direttamente le azioni da compiere in risposta ad un messaggio.

Un altro esempio è l’architettura MVC (Modello-View-Controller): La view si aggiorna quando il modello cambia. Uno dei vantaggi dell’uso di MVC è il disaccoppiamento della vista dal modello per ridurre le dipendenze.

Come si evince dalla figura in basso, il Subject contiene i riferimenti alle istanze Concrete Observer per notificare loro eventuali messaggi. L’Observer è una classe astratta che permette ai Concrete Observer (A e B) di implementare il metodo di notifica, ognuno secondo le proprie necessità.

1000px-observer-svg

Uno degli svantaggi dell’Observer Pattern è il calo significativo delle prestazioni all’aumentare del numero di Concrete Observer. In AngularJS, un esempio di Observer frequentemente utilizzato sono i cosidetti watchers (che sono anche una causa del calo di prestazioni in AngularJS): gni volta che leghiamo un elemento dell’interfaccia utente ad una variabile del modello dei dati, Angular tiene sotto controllo il suo valore creando un cosiddetto watch. Quindi in pratica siamo in grado di guardare quasiasi variabile, funzione e oggetto. Il digest loop scorre la lista dei watch allo scopo di individuare le variabili il cui valore è cambiato dall’ultima esecuzione del loop. Se il valore di una variabile è cambiato, vengono eseguite le opportune attività che consentono, ad esempio, di aggiornare il DOM nei punti in cui la variabile è utilizzata.

Tornando all’Observer Pattern, ecco un esempio riepilogativo:

var Subject = function() {
  observers = [];

  return {
    iscriviObserver: function(observer) {
      observers.push(observer);
    },
    cancellaObserver: function(observer) {
      var index = observers.indexOf(observer);
      if(index > -1) {
        observers.splice(index, 1);
      }
    },
    notificaObserver: function(observer) {
      var index = observers.indexOf(observer);
      if(index > -1) {
        observers[index].notifica(index);
      }
    },
    notificaTuttiObservers: function() {
      for(var i = 0; i < observers.length; i++){
        observers[i].notifica(i);
      };
    }
  };
};

var Observer = function() {
  return {
    notifica: function(index) {
      console.log("Observer " + index + " è stato notificato!");
    }
  }
}

var subject = new Subject();

var observer1 = new Observer();
var observer2 = new Observer();
var observer3 = new Observer();
var observer4 = new Observer();

subject.iscriviObserver(observer1);
subject.iscriviObserver(observer2);
subject.iscriviObserver(observer3);
subject.iscriviObserver(observer4);

subject.notificaObserver(observer2); // Observer 1 è stato notificato!

subject.notificaTuttiObservers();
// Observer 0 è stato notificato!
// Observer 1 è stato notificato!
// Observer 2 è stato notificato!
// Observer 3 è stato notificato!

Design Pattern Singleton

Il singleton rappresenta un tipo particolare di classe che ha lo scopo di garantire che venga creata una e una sola istanza di una determinata classe  e di fornire un punto di accesso globale a tale istanza all’interno di un programma, da qui il nome Singleton…

Un esempio classico è quello della stampante di un ufficio: ci sono dieci persone in un ufficio che usano ciascuno un personal computer mentre tutti usano la sola stampante disponibile, condividendo di fatto la stampante e quindi la stessa risorsa. L’esempio può anche essere implementato in questo modo:

var stampante = (function () {

  var istanzaDiStampante;

  function creaStampante() {

    function stampa() {
      // qui va il comando per la stampa
    }

    function accendi() {
      // qui va il boot della stampante
    }

    return {
      stampa: stampa,
      accendi: accendi
    };
  }

  return {
    getInstance: function() {
      if(!istanzaDiStampante) {
        istanzaDiStampante= creaStampante();
      }
      return istanzaDiStampante;
    }
  };

  function Singleton() {
    if(!istanzaDiStampante) {
       istanzaDiStampante = intialize();
    }
  };

})();

Il metodo creaStampante() è privato perchè non vogliamo che i “client” possano accedere ad esso direttamente, mentre, rendiamo pubblico il metodo getIstance() in modo da creare l’istanza della stampante solo se quest’ultima non esiste, altrimenti ritorniamo l’istanza esistente. Ciascun utente può quindi generare un’istanza di stampante, interagendo direttamente col il metodo getIstance(), in questo modo

var officePrinter = printer.getInstance();

 

Conclusioni

I design pattern sono solitamente utilizzati nelle applicazioni di grandi dimensioni. Per capire effettivamente le potenzialità e i vantaggi derivati dall’uso dei pattern, bisognerebbe quindi testarli praticamente. Ovviamente, prima di implementare qualsiasi applicazione, si dovrebbe pensare a quali sono gli attori e come questi interagiscono tra di loro. Dopo aver esaminato i pattern Module, Prototype, Observer, e Singleton, si dovrebbe essere in grado di identificarli e utilizzarli correttamente.