Javascript – 4 design pattern che dovresti conoscere
Ogni sviluppatore si sforza di scrivere codice manutenibile, leggibile e riusabile. La strutturazione del codice diventa più importante man mano che l’applicazione diventa più grande. I Design patterns si dimostrano cruciali per rispondere a questa sfida – fornendo una struttura organizzativa adatta alle circostanze.
Gli sviluppatori javascript frequentemente interagiscono con design patterns quando creano un’applicazione, senza neanche saperlo. Alcuni design pattern sono più frequenti di altri.
In questo post vorrei discutere dei pattern comuni in modo da accrescere il proprio repertorio e scrivere codice migliore.
I design pattern in questione includono:
– Module
– Prototype
– Observer
– Singleton
Ogni pattern possiede diverse proprietà, tuttavia ci concentreremo essenzialmente su questi punti chiave:
1. Contesto: Dove e sotto quali circostanze viene usato il pattern?
2. Problema: Quale problema stiamo cercando di risolvere?
3. Soluzione: Come fa il nostro pattern a risolvere il problema proposto?
4. Implementazione: Come appare l’implementazione?
Design Pattern: Module
I moduli javascript rappresentano il design pattern più diffuso per mantenere un particolare pezzo di codice indipendente da altri componenti. Questo garantisce uno scarso accoppiamento in favore di un codice più strutturato.
Per quelli che sono familiari ai linguaggi orientati agli oggetti, i moduli sono le “classi” di javascript. Uno dei molti vantaggi delle classi è l’incapsulamento – ovvero la protezione degli stati e dei metodi in modo che non vengano utilizzati da altre classi. Il Module pattern permette livelli d’accesso pubblici e privati (così come i meno conosciuti “protected” e “privileged”).
I moduli dovrebbero essere all’interno di blocchi IIFE (Immediately-Inovked-Function-Expressions) per permettere uno scope privato: una chiusura che protegge appunto variabili e metodi dallo scope più esterno. Il blocco in questione restituisce solitamente un oggetto piuttosto che una funzione. Ecco come si presenta:
(function() { // qui dichiariamo variabili e/o funzioni private! return { // qui dichiariamo variabili e/o funzioni pubbliche! } })(); |
Qui istanziamo le variabili e/o le funzioni private prima di restituire l’oggetto voluto. Il codice al di fuori della closure non è in grado di accedere alle variabili private dal momento che non appartengono allo stesso scope. Consideriamo adesso un’implementazione più concreta:
var HTMLChanger = (function() { var contents = 'contents' var changeHTML = function() { var element = document.getElementById('attribute-to-change'); element.innerHTML = contents; } return { callChangeHTML: function() { changeHTML(); console.log(contents); } }; })(); HTMLChanger.callChangeHTML(); // Outputs: 'contents' console.log(HTMLChanger.contents); // undefined |
Notiamo come callChangeHTML
sia restituito e possa essere referenziato dalla variabile HTMLChanger
. Tuttavia, se siamo fuori dal modulo e vogliamo ad esempio stampare il contenuto di contents
questo non è referenziato e disponibile all’infuori dal modulo (IIFE).
Una variante di questo design pattern è chiamata Revealing Module Pattern.
Revealing Module Pattern
Lo scopo è di mantenere l’incapsulamento e rivelare alcune variabili e metodi restituiti in un oggetto. L’implementazione diretta è questa:
var Exposer = (function() { var privateVariable = 10; var privateMethod = function() { console.log('Inside a private method!'); privateVariable++; } var methodToExpose = function() { console.log('This is a method I want to expose!'); } var otherMethodIWantToExpose = function() { privateMethod(); } return { first: methodToExpose, second: otherMethodIWantToExpose }; })(); Exposer.first(); // Output: This is a method I want to expose! Exposer.second(); // Output: Inside a private method! Exposer.methodToExpose; // undefined |
Anche se può sembrare molto più pulita come soluzione l’ovvio svantaggio è che non siamo in grado di referenziare i metodi privati. Dunque introduce nuove sfide specie se volessimo effettuare dei test unitari.
Design Pattern: prototype
Qualunque sviluppatore Javascript avrà visto la keyword prototype almeno una volta. Il design pattern prototype, sfrutta proprio l’eredità prototipale di Javascript. Il modello prototype è usato principalmente per la creazione di oggetti in situazioni in cui si richiedono alte performance.
Un costruttore è sempre necessario se vogliamo istanziare un oggetto. Attraverso la keyword prototype possiamo associare variabili e metodi alla struttura dell’oggetto. Guardiamo un esempio base:
var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; } TeslaModelS.prototype.go = function() { // Rotate wheels } TeslaModelS.prototype.stop = function() { // Apply brake pads } |
Il costruttore ci permette di creare un singolo oggetto TeslaModelS. Quando creiamo un nuovo oggetto TestlaModeS, conserverà gli stati inizializzati nel costruttore. Inoltre, manterrà le funzioni ‘go’ e ‘stop’ dal momento che le abbiamo dichiarate dal prototipo. Un altro modo, differente nella forma ma identico nel comportamento è questo:
var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; } TeslaModelS.prototype = { go: function() { // Rotate wheels }, stop: function() { // Apply brake pads } } |
Revealing Prototype Pattern
Similmente al Module Pattern, il Prototype Pattern ha una leggera variante. Il revealing prototype pattern fornisce incapsulazione con i membri pubblici e privati.
Dal momento che stiamo restituendo un oggetto, prefissiamo l’oggetto prototype con una funzione. Estendendo l’esempio sopra, possiamo scegliere cosa esporre e cosa non esporre:
var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; } TeslaModelS.prototype = function() { var go = function() { // Rotate wheels }; var stop = function() { // Apply brake pads }; return { pressBrakePedal: stop, pressGasPedal: go } }(); |
Notare come le funzioni go e stop saranno protette dall’oggetto restituito perchè fuori dallo scope dell’oggetto stesso. Javascript supporta nativamente l’eredità prototipale, non c’è bisogno di scrivere nient’altro.
Design Pattern: Observer
Molte volte quando in un’applicazione cambia una parte di essa, altre parti necessitano di essere aggiornate. In AngularJS, se l’oggetto $scope
si aggiorna, un evento può essere emesso per avvisare gli altri componenti. Il pattern dell’osservatore incorpora proprio questo: se un oggetto è modificato, fa sapere agli altri oggetti dipendenti che qualcosa è cambiato.
Un primo esempio è proprio l’architettura MVC (model-view-controller); La vista viene aggiornata quando un modello cambia. Un primo beneficio è quello di disaccoppiare le viste dai modelli, così da ridurre le dipendenze.
Diamo una prima occhiata a questo esempio in AngularJS per capire come Angular racchiuda l’Observer pattern attraverso la gestione degli eventi:
// Controller 1 $scope.$on('nameChanged', function(event, args) { $scope.name = args.name; }); ... // Controller 2 $scope.userNameChanged = function(name) { $scope.$emit('nameChanged', {name: name}); }; |
E’ importante notare che anche se l’observer pattern offre molti vantaggi, uno degli svantaggi è un significante calo in termini di performance man mano che il numero degli osservatori cresce.
In AngularJS possiamo “osservare” variabili, funzioni e oggetti.
In javascript possiamo creare i nostri “Soggetti” e “Osservatori”. Vediamo come si implementa:
var Subject = function() { this.observers = []; return { subscribeObserver: function(observer) { this.observers.push(observer); }, unsubscribeObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers.splice(index, 1); } }, notifyObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers[index].notify(index); } }, notifyAllObservers: function() { for(var i = 0; i < this.observers.length; i++){ this.observers[i].notify(i); }; } }; }; var Observer = function() { return { notify: function(index) { console.log("Observer " + index + " is notified!"); } } } var subject = new Subject(); var observer1 = new Observer(); var observer2 = new Observer(); var observer3 = new Observer(); var observer4 = new Observer(); subject.subscribeObserver(observer1); subject.subscribeObserver(observer2); subject.subscribeObserver(observer3); subject.subscribeObserver(observer4); subject.notifyObserver(observer2); // Observer 2 is notified! subject.notifyAllObservers(); // Observer 1 is notified! // Observer 2 is notified! // Observer 3 is notified! // Observer 4 is notified! |
Publish/Subscribe
Il pattern publish/subscribe (produttore/consumatore) è un pattern simile a quello dell’Observer, basato sugli eventi. Questo sistema di eventi permette di definire eventi tipici per l’applicazione, che possano passare argomenti specifici necessari ai sottoscrittori (consumatori). L’idea è quella di evitare le dipendenze fra subscriber e publisher usando un canale topic/evento.
Differisce con l’Observer pattern dal momento che qualunque subscriber implementa la propria gestione degli eventi e ricevere notifiche ai topic presso cui è sottoscritto, emesse dai publisher.
Molti sviluppatori tendono ad aggregare l’observer pattern e il publish/subscribe, tuttavia c’è una distinzione.
In AngularJS, un subscriber si sottoscrive ad un evento usando $on('event', callback)
, e un publisher emette un evento usando $emit('event', args)
oppure $broadcast('event', args)
.
Design Pattern: Singleton
Un Singleton, come il nome stesso già evoca, permette una sola instanziazione, ma più istanze dello stesso oggetto.
Il Singleton vieta ai client la creazione di oggetti multipli, dopo la creazione del primo oggetto infatti restituirà istanze di se stesso.
E’ difficile trovare dei casi d’uso per il Singleton. Facciamo un esempio considerando la stampante di un ufficio.
Se ci sono dieci persone in un ufficio, e usano tutte una singola stampante, dieci computer condivideranno una sola stampante (una sola istanza). Condividendo una stampante, condivideranno le stesse risorse.
var printer = (function () { var printerInstance; function create () { function print() { // underlying printer mechanics } function turnOn() { // warm up // check for paper } return { // public + private states and behaviors print: print, turnOn: turnOn }; } return { getInstance: function() { if(!printerInstance) { printerInstance = create(); } return printerInstance; } }; function Singleton () { if(!printerInstance) { printerInstance = intialize(); } }; })(); |
Il metodo create
è privato perchè non vogliamo che il client possa accedervi, tuttavia, notiamo come il metodo getInstance
sia pubblico.
Ogni persona nell’ufficio può generare un’istanza della stampante (printer) interagendo con il metodo getInstance
, così:
var officePrinter = printer.getInstance(); |
In applicazioni multi-thread possono capitare delle “race conditions” quando più thread cercano di accedere contemporaneamente alla stessa risorsa. I singleton sono suscettibili alle race conditions, per esempio se non esiste ancora un’istanza, due thread potrebbero accedere contemporaneamente e dunque generare due istanze, verrebbe meno dunque lo scopo del singleton.
E’ importante che lo sviluppatore implementi delle soluzioni sincrone in questi casi.
Conclusioni
I Design patterns sono frequentemente usati in grandi applicazioni. Capire quale sia il più adatto e il più vantaggioso in una data applicazione è un qualcosa che arriva con la pratica.
Prima di realizzare qualunque applicazione bisogna avere ben chiaro chi siano gli attori e come questi interagiscano fra di loro.
Detto questo per qualunque dubbio invito a commentare sotto. Buon coding.
Commenti