La guida completa ai modelli di progettazione JavaScript

Pubblicato: 2022-03-11

In qualità di buon sviluppatore JavaScript, ti sforzi di scrivere codice pulito, sano e manutenibile. Risolvi sfide interessanti che, sebbene uniche, non richiedono necessariamente soluzioni uniche. Probabilmente ti sei trovato a scrivere un codice simile alla soluzione di un problema completamente diverso che hai gestito in precedenza. Potresti non saperlo, ma hai utilizzato un modello di progettazione JavaScript . I modelli di progettazione sono soluzioni riutilizzabili ai problemi che si verificano comunemente nella progettazione del software.

La guida completa ai modelli di progettazione JavaScript

Durante la vita di qualsiasi lingua, molte di queste soluzioni riutilizzabili sono realizzate e testate da un gran numero di sviluppatori della comunità di quella lingua. È grazie a questa esperienza combinata di molti sviluppatori che tali soluzioni sono così utili perché ci aiutano a scrivere codice in modo ottimizzato mentre allo stesso tempo risolvono il problema a portata di mano.

I principali vantaggi che otteniamo dai modelli di progettazione sono i seguenti:

  • Sono soluzioni comprovate: poiché i modelli di progettazione sono spesso utilizzati da molti sviluppatori, puoi essere certo che funzionino. E non solo, puoi essere certo che sono state riviste più volte e probabilmente sono state implementate ottimizzazioni.
  • Sono facilmente riutilizzabili: i modelli di progettazione documentano una soluzione riutilizzabile che può essere modificata per risolvere più problemi particolari, poiché non sono legati a un problema specifico.
  • Sono espressivi: i modelli di design possono spiegare una soluzione di grandi dimensioni in modo abbastanza elegante.
  • Facilitano la comunicazione: quando gli sviluppatori hanno familiarità con i modelli di progettazione, possono comunicare più facilmente tra loro sulle potenziali soluzioni a un determinato problema.
  • Prevengono la necessità di refactoring del codice: se un'applicazione viene scritta tenendo conto dei design pattern, spesso non sarà necessario rifattorizzare il codice in un secondo momento perché applicare il design pattern corretto a un determinato problema è già un'ottima soluzione soluzione.
  • Riducono le dimensioni della base di codice: poiché i modelli di progettazione sono generalmente soluzioni eleganti e ottimali, di solito richiedono meno codice rispetto ad altre soluzioni.

So che sei pronto per entrare a questo punto, ma prima di imparare tutto sui modelli di progettazione, esaminiamo alcune nozioni di base su JavaScript.

Una breve storia di JavaScript

JavaScript è oggi uno dei linguaggi di programmazione più popolari per lo sviluppo web. Inizialmente è stato realizzato come una sorta di "colla" per vari elementi HTML visualizzati, noto come linguaggio di scripting lato client, per uno dei browser Web iniziali. Chiamato Netscape Navigator, all'epoca poteva visualizzare solo HTML statico. Come si può supporre, l'idea di un tale linguaggio di scripting ha portato a guerre di browser tra i grandi attori nel settore dello sviluppo di browser all'epoca, come Netscape Communications (oggi Mozilla), Microsoft e altri.

Ciascuno dei grandi giocatori voleva portare avanti la propria implementazione di questo linguaggio di scripting, quindi Netscape ha creato JavaScript (in realtà, Brendan Eich), Microsoft ha creato JScript e così via. Come puoi immaginare, le differenze tra queste implementazioni erano notevoli, quindi lo sviluppo per i browser Web è stato effettuato per browser, con gli adesivi più visualizzati forniti con una pagina Web. Ben presto è diventato chiaro che avevamo bisogno di uno standard, una soluzione cross-browser che unificasse il processo di sviluppo e semplificasse la creazione di pagine web. Quello che hanno inventato si chiama ECMAScript.

ECMAScript è una specifica standardizzata del linguaggio di scripting che tutti i browser moderni cercano di supportare, e ci sono molteplici implementazioni (potreste dire dialetti) di ECMAScript. Il più popolare è l'argomento di questo articolo, JavaScript. Sin dal suo rilascio iniziale, ECMAScript ha standardizzato molte cose importanti e, per coloro che sono più interessati alle specifiche, è disponibile un elenco dettagliato di elementi standardizzati per ciascuna versione di ECMAScript su Wikipedia. Il supporto del browser per ECMAScript versioni 6 (ES6) e successive è ancora incompleto e deve essere trasferito a ES5 per essere completamente supportato.

Cos'è JavaScript?

Per comprendere appieno i contenuti di questo articolo, facciamo un'introduzione ad alcune caratteristiche del linguaggio molto importanti di cui dobbiamo essere a conoscenza prima di immergerci nei modelli di progettazione JavaScript. Se qualcuno ti chiedesse "Cos'è JavaScript?" potresti rispondere da qualche parte nelle righe di:

JavaScript è un linguaggio di programmazione leggero, interpretato e orientato agli oggetti con funzioni di prima classe più comunemente note come linguaggio di scripting per le pagine web.

La definizione di cui sopra significa che il codice JavaScript ha un'impronta di memoria ridotta, è facile da implementare e facile da imparare, con una sintassi simile a linguaggi popolari come C++ e Java. È un linguaggio di scripting, il che significa che il suo codice viene interpretato anziché compilato. Supporta stili di programmazione procedurali, orientati agli oggetti e funzionali, il che lo rende molto flessibile per gli sviluppatori.

Finora, abbiamo esaminato tutte le caratteristiche che suonano come molte altre lingue là fuori, quindi diamo un'occhiata a ciò che è specifico di JavaScript rispetto ad altre lingue. Elencherò alcune caratteristiche e darò il mio meglio per spiegare perché meritano un'attenzione speciale.

JavaScript supporta funzioni di prima classe

Questa caratteristica era difficile da capire per me quando stavo appena iniziando con JavaScript, poiché provenivo da un background C/C++. JavaScript tratta le funzioni come cittadini di prima classe, il che significa che puoi passare funzioni come parametri ad altre funzioni proprio come faresti con qualsiasi altra variabile.

 // we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })

JavaScript è basato su prototipi

Come nel caso di molti altri linguaggi orientati agli oggetti, JavaScript supporta gli oggetti e uno dei primi termini che viene in mente quando si pensa agli oggetti è classi ed ereditarietà. È qui che diventa un po' complicato, poiché il linguaggio non supporta le classi nella sua forma in linguaggio semplice, ma usa piuttosto qualcosa chiamato ereditarietà basata su prototipi o basata su istanze.

È solo ora, in ES6, che viene introdotto il termine formale classe , il che significa che i browser non lo supportano ancora (se ricordi, al momento della stesura, l'ultima versione di ECMAScript completamente supportata è 5.1). È importante notare, tuttavia, che anche se il termine "classe" è stato introdotto in JavaScript, utilizza ancora l'ereditarietà basata su prototipi sotto il cofano.

La programmazione basata su prototipi è uno stile di programmazione orientata agli oggetti in cui il riutilizzo del comportamento (noto come ereditarietà) viene eseguito tramite un processo di riutilizzo di oggetti esistenti tramite deleghe che fungono da prototipi. Ci addentreremo più in dettaglio con questo una volta arrivati ​​alla sezione dei modelli di progettazione dell'articolo, poiché questa caratteristica è utilizzata in molti modelli di progettazione JavaScript.

JavaScript cicli di eventi

Se hai esperienza di lavoro con JavaScript, hai sicuramente familiarità con il termine funzione di callback . Per coloro che non hanno familiarità con il termine, una funzione di callback è una funzione inviata come parametro (ricorda, JavaScript tratta le funzioni come cittadini di prima classe) a un'altra funzione e viene eseguita dopo l'attivazione di un evento. Di solito viene utilizzato per iscriversi a eventi come un clic del mouse o la pressione di un pulsante della tastiera.

Rappresentazione grafica del ciclo di eventi JavaScript

Ogni volta che un evento, a cui è collegato un listener, si attiva (altrimenti l'evento viene perso), viene inviato un messaggio a una coda di messaggi che vengono elaborati in modo sincrono, in modalità FIFO (first-in-first-out ). Questo è chiamato ciclo di eventi .

Ciascun messaggio nella coda ha una funzione ad esso associata. Una volta che un messaggio è stato rimosso dalla coda, il runtime esegue completamente la funzione prima di elaborare qualsiasi altro messaggio. Vale a dire, se una funzione contiene altre chiamate di funzione, vengono eseguite tutte prima dell'elaborazione di un nuovo messaggio dalla coda. Questo è chiamato run-to-completamento.

 while (queue.waitForMessage()) { queue.processNextMessage(); }

queue.waitForMessage() attende in modo sincrono nuovi messaggi. Ciascuno dei messaggi in elaborazione ha il proprio stack e viene elaborato finché lo stack non è vuoto. Al termine, un nuovo messaggio viene elaborato dalla coda, se presente.

Potresti anche aver sentito che JavaScript non è bloccante, il che significa che quando viene eseguita un'operazione asincrona, il programma è in grado di elaborare altre cose, come la ricezione dell'input dell'utente, mentre attende il completamento dell'operazione asincrona, non bloccando il thread di esecuzione. Questa è una proprietà molto utile di JavaScript e un intero articolo potrebbe essere scritto proprio su questo argomento; tuttavia, è al di fuori dell'ambito di questo articolo.

Cosa sono i modelli di progettazione?

Come ho detto prima, i modelli di progettazione sono soluzioni riutilizzabili ai problemi che si verificano comunemente nella progettazione del software. Diamo un'occhiata ad alcune delle categorie di modelli di progettazione.

Proto-modelli

Come si crea un modello? Diciamo che hai riconosciuto un problema che si verifica comunemente e hai la tua soluzione unica a questo problema, che non è riconosciuta e documentata a livello globale. Usi questa soluzione ogni volta che incontri questo problema e pensi che sia riutilizzabile e che la comunità di sviluppatori possa trarne vantaggio.

Diventa subito uno schema? Per fortuna, no. Spesso, si possono avere buone pratiche di scrittura del codice e semplicemente scambiare qualcosa che sembra un modello per uno quando, in realtà, non è un modello.

Come puoi sapere quando ciò che pensi di riconoscere è in realtà un modello di progettazione?

Ottenendo le opinioni di altri sviluppatori al riguardo, conoscendo il processo di creazione di un modello stesso e conoscendo bene i modelli esistenti. C'è una fase che un modello deve attraversare prima che diventi un modello a tutti gli effetti, e questo è chiamato un proto-modello.

Un proto-modello è un modello futuro se supera un certo periodo di test da parte di vari sviluppatori e scenari in cui il modello si rivela utile e fornisce risultati corretti. C'è una grande quantità di lavoro e documentazione, la maggior parte della quale non rientra nell'ambito di questo articolo, da fare per creare un modello a tutti gli effetti riconosciuto dalla comunità.

Anti-modelli

Poiché un modello di progettazione rappresenta una buona pratica, un antimodello rappresenta una cattiva pratica.

Un esempio di anti-pattern potrebbe essere la modifica del prototipo della classe Object . Quasi tutti gli oggetti in JavaScript ereditano da Object (ricorda che JavaScript utilizza l'ereditarietà basata su prototipi), quindi immagina uno scenario in cui hai alterato questo prototipo. Le modifiche al prototipo Object verrebbero visualizzate in tutti gli oggetti che ereditano da questo prototipo , che sarebbero la maggior parte degli oggetti JavaScript . Questo è un disastro in attesa di accadere.

Un altro esempio, simile a quello menzionato sopra, è la modifica di oggetti che non possiedi. Un esempio potrebbe essere l'override di una funzione da un oggetto utilizzato in molti scenari nell'applicazione. Se stai lavorando con una grande squadra, immagina la confusione che ciò causerebbe; ti imbatteresti rapidamente in conflitti di denominazione, implementazioni incompatibili e incubi di manutenzione.

Simile a come è utile conoscere tutte le buone pratiche e soluzioni, è anche molto importante conoscere anche quelle cattive. In questo modo, puoi riconoscerli ed evitare di commettere l'errore in anticipo.

Categorizzazione del modello di progettazione

I modelli di progettazione possono essere classificati in diversi modi, ma il più popolare è il seguente:

  • Modelli di progettazione creativa
  • Modelli di progettazione strutturale
  • Modelli di progettazione comportamentale
  • Modelli di progettazione della concorrenza
  • Modelli di progettazione architettonica

Modelli di progettazione creativa

Questi modelli si occupano di meccanismi di creazione di oggetti che ottimizzano la creazione di oggetti rispetto a un approccio di base. La forma base della creazione di oggetti potrebbe causare problemi di progettazione o una maggiore complessità del design. I modelli di progettazione della creazione risolvono questo problema controllando in qualche modo la creazione di oggetti. Alcuni dei modelli di design popolari in questa categoria sono:

  • Metodo di fabbrica
  • Fabbrica astratta
  • Costruttore
  • Prototipo
  • Singleton

Modelli di progettazione strutturale

Questi modelli riguardano le relazioni oggettuali. Garantiscono che se una parte di un sistema cambia, l'intero sistema non deve cambiare insieme ad essa. I modelli più popolari in questa categoria sono:

  • Adattatore
  • Ponte
  • Composito
  • Decoratore
  • Facciata
  • Peso mosca
  • procuratore

Modelli di progettazione comportamentale

Questi tipi di modelli riconoscono, implementano e migliorano la comunicazione tra oggetti disparati in un sistema. Aiutano a garantire che parti disparate di un sistema abbiano informazioni sincronizzate. Esempi popolari di questi modelli sono:

  • Catena di responsabilità
  • Comando
  • Iteratore
  • Mediatore
  • ricordo
  • Osservatore
  • Stato
  • Strategia
  • Visitatore

Modelli di progettazione della concorrenza

Questi tipi di modelli di progettazione si occupano di paradigmi di programmazione multi-thread. Alcuni di quelli popolari sono:

  • Oggetto attivo
  • Reazione nucleare
  • Pianificatore

Modelli di progettazione architettonica

Modelli di design utilizzati per scopi architettonici. Alcuni dei più famosi sono:

  • MVC (Model-View-Controller)
  • MVP (Model-View-Presenter)
  • MVVM (Model-View-ViewModel)

Nella sezione seguente, daremo un'occhiata più da vicino ad alcuni dei modelli di progettazione sopra menzionati con esempi forniti per una migliore comprensione.

Esempi di modelli di progettazione

Ciascuno dei modelli di progettazione rappresenta un tipo specifico di soluzione a un tipo specifico di problema. Non esiste un insieme universale di modelli che sia sempre la soluzione migliore. Dobbiamo imparare quando un particolare modello si rivelerà utile e se fornirà un valore effettivo. Una volta che abbiamo acquisito familiarità con i modelli e gli scenari per i quali sono più adatti, possiamo facilmente determinare se un modello specifico si adatta o meno a un determinato problema.

Ricorda, l'applicazione del modello sbagliato a un determinato problema potrebbe portare a effetti indesiderati come complessità del codice non necessaria, sovraccarico non necessario sulle prestazioni o persino la generazione di un nuovo anti-pattern.

Queste sono tutte cose importanti da considerare quando si pensa di applicare un design pattern al nostro codice. Daremo un'occhiata ad alcuni dei modelli di progettazione che personalmente ho trovato utili e credo che ogni sviluppatore JavaScript senior dovrebbe avere familiarità.

Modello del costruttore

Quando si pensa ai classici linguaggi orientati agli oggetti, un costruttore è una funzione speciale in una classe che inizializza un oggetto con una serie di valori predefiniti e/o inviati.

I modi comuni per creare oggetti in JavaScript sono i tre modi seguenti:

 // either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();

Dopo aver creato un oggetto, ci sono quattro modi (da ES3) per aggiungere proprietà a questi oggetti. Sono i seguenti:

 // supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });

Il modo più diffuso per creare oggetti sono le parentesi graffe e, per aggiungere proprietà, la notazione punto o parentesi quadre. Chiunque abbia esperienza con JavaScript li ha usati.

Abbiamo accennato in precedenza che JavaScript non supporta le classi native, ma supporta i costruttori attraverso l'uso di una parola chiave "nuova" preceduta da una chiamata di funzione. In questo modo, possiamo usare la funzione come costruttore e inizializzare le sue proprietà allo stesso modo di un classico costruttore di linguaggio.

 // we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();

Tuttavia, c'è ancora spazio per miglioramenti qui. Se ricorderete, ho menzionato in precedenza che JavaScript utilizza l'ereditarietà basata su prototipi. Il problema con l'approccio precedente è che il metodo writesCode viene ridefinito per ciascuna delle istanze del costruttore Person . Possiamo evitarlo impostando il metodo nel prototipo della funzione:

 // we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();

Ora, entrambe le istanze del costruttore Person possono accedere a un'istanza condivisa del metodo writesCode() .

Modello del modulo

Per quanto riguarda le particolarità, JavaScript non smette mai di stupire. Un'altra cosa peculiare di JavaScript (almeno per quanto riguarda i linguaggi orientati agli oggetti) è che JavaScript non supporta i modificatori di accesso. In un linguaggio OOP classico, un utente definisce una classe e determina i diritti di accesso per i suoi membri. Poiché JavaScript nella sua forma semplice non supporta né classi né modificatori di accesso, gli sviluppatori JavaScript hanno trovato un modo per imitare questo comportamento quando necessario.

Prima di entrare nello specifico del pattern del modulo, parliamo del concetto di chiusura. Una chiusura è una funzione con accesso all'ambito padre, anche dopo la chiusura della funzione padre. Ci aiutano a imitare il comportamento dei modificatori di accesso attraverso l'ambito. Mostriamolo attraverso un esempio:

 // we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());

Come puoi vedere, usando l'IIFE, abbiamo legato la variabile counter a una funzione che è stata invocata e chiusa ma è ancora accessibile dalla funzione figlio che la incrementa. Poiché non possiamo accedere alla variabile contatore dall'esterno dell'espressione della funzione, l'abbiamo resa privata tramite la manipolazione dell'ambito.

Utilizzando le chiusure, possiamo creare oggetti con parti private e pubbliche. Questi sono chiamati moduli e sono molto utili ogni volta che vogliamo nascondere alcune parti di un oggetto ed esporre un'interfaccia solo all'utente del modulo. Mostriamolo in un esempio:

 // through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());

La cosa più utile introdotta da questo modello è la netta separazione delle parti private e pubbliche di un oggetto, che è un concetto molto simile agli sviluppatori provenienti da un classico background orientato agli oggetti.

Tuttavia, non tutto è così perfetto. Quando si desidera modificare la visibilità di un membro, è necessario modificare il codice ovunque sia stato utilizzato questo membro a causa della diversa natura di accesso alle parti pubbliche e private. Inoltre, i metodi aggiunti all'oggetto dopo la loro creazione non possono accedere ai membri privati ​​dell'oggetto.

Rivelando il modello del modulo

Questo modello è un miglioramento apportato al modello del modulo come illustrato sopra. La differenza principale è che scriviamo l'intera logica dell'oggetto nell'ambito privato del modulo e quindi esponiamo semplicemente le parti che vogliamo rendere pubbliche restituendo un oggetto anonimo. Possiamo anche modificare la denominazione dei membri privati ​​durante la mappatura dei membri privati ​​sui membri pubblici corrispondenti.

 // we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());

Il pattern del modulo rivelatore è uno degli almeno tre modi in cui possiamo implementare un pattern del modulo. Le differenze tra il pattern del modulo rivelatore e le altre varianti del pattern del modulo sono principalmente nel modo in cui vengono referenziati i membri pubblici. Di conseguenza, il pattern del modulo rivelatore è molto più facile da usare e modificare; tuttavia, potrebbe rivelarsi fragile in alcuni scenari, come l'utilizzo di oggetti RMP come prototipi in una catena di ereditarietà. Le situazioni problematiche sono le seguenti:

  1. Se abbiamo una funzione privata che si riferisce a una funzione pubblica, non possiamo sovrascrivere la funzione pubblica, poiché la funzione privata continuerà a fare riferimento all'implementazione privata della funzione, introducendo così un bug nel nostro sistema.
  2. Se abbiamo un membro pubblico che punta a una variabile privata e proviamo a sovrascrivere il membro pubblico dall'esterno del modulo, le altre funzioni farebbero comunque riferimento al valore privato della variabile, introducendo un bug nel nostro sistema.

Modello singleton

Il modello singleton viene utilizzato negli scenari in cui è necessaria esattamente un'istanza di una classe. Ad esempio, abbiamo bisogno di un oggetto che contenga una configurazione per qualcosa. In questi casi, non è necessario creare un nuovo oggetto ogni volta che l'oggetto di configurazione è richiesto da qualche parte nel sistema.

 var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);

Come puoi vedere nell'esempio, il numero casuale generato è sempre lo stesso, così come i valori di configurazione inviati.

È importante notare che il punto di accesso per recuperare il valore singleton deve essere solo uno e molto noto. Uno svantaggio dell'utilizzo di questo modello è che è piuttosto difficile da testare.

Modello dell'osservatore

Il pattern osservatore è uno strumento molto utile quando abbiamo uno scenario in cui dobbiamo migliorare la comunicazione tra parti disparate del nostro sistema in modo ottimizzato. Favorisce l'accoppiamento libero tra gli oggetti.

Esistono varie versioni di questo modello, ma nella sua forma più elementare abbiamo due parti principali del modello. Il primo è un soggetto e il secondo sono gli osservatori.

Un soggetto gestisce tutte le operazioni riguardanti un determinato argomento a cui gli osservatori si iscrivono. Queste operazioni iscrivono un osservatore a un determinato argomento, annullano l'iscrizione a un osservatore da un determinato argomento e notificano agli osservatori un determinato argomento quando viene pubblicato un evento.

Tuttavia, esiste una variazione di questo modello chiamato modello editore/abbonato, che userò come esempio in questa sezione. La principale differenza tra un modello osservatore classico e il modello editore/abbonato è che editore/abbonato promuove un accoppiamento ancora più sciolto rispetto al modello osservatore.

Nel modello osservatore, il soggetto mantiene i riferimenti agli osservatori iscritti e chiama i metodi direttamente dagli oggetti stessi mentre, nel modello editore/abbonato, abbiamo canali, che fungono da ponte di comunicazione tra un abbonato e un editore. L'editore attiva un evento ed esegue semplicemente la funzione di callback inviata per quell'evento.

Mostrerò un breve esempio del modello editore/abbonato, ma per chi fosse interessato, un classico esempio di modello osservatore può essere facilmente trovato online.

 var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});

Questo modello di progettazione è utile nelle situazioni in cui è necessario eseguire più operazioni su un singolo evento che viene attivato. Immagina di avere uno scenario in cui è necessario effettuare più chiamate AJAX a un servizio di back-end e quindi eseguire altre chiamate AJAX a seconda del risultato. Dovresti annidare le chiamate AJAX l'una nell'altra, eventualmente entrando in una situazione nota come callback hell. L'uso del modello editore/abbonato è una soluzione molto più elegante.

Uno svantaggio dell'utilizzo di questo modello è il difficile test di varie parti del nostro sistema. Non esiste un modo elegante per sapere se le parti del sistema di sottoscrizione si comportano o meno come previsto.

Modello mediatore

Tratteremo brevemente uno schema che è anche molto utile quando si parla di sistemi disaccoppiati. Quando abbiamo uno scenario in cui più parti di un sistema devono comunicare ed essere coordinate, forse una buona soluzione sarebbe introdurre un mediatore.

Un mediatore è un oggetto che viene utilizzato come punto centrale per la comunicazione tra parti disparate di un sistema e gestisce il flusso di lavoro tra di loro. Ora, è importante sottolineare che gestisce il flusso di lavoro. Perché questo è importante?

Perché c'è una grande somiglianza con il modello editore/abbonato. Potresti chiederti, OK, quindi questi due schemi aiutano entrambi a implementare una migliore comunicazione tra gli oggetti... Qual è la differenza?

La differenza è che un mediatore gestisce il flusso di lavoro, mentre l'editore/abbonato utilizza un tipo di comunicazione chiamato "spara e dimentica". L'editore/abbonato è semplicemente un aggregatore di eventi, il che significa che si occupa semplicemente di attivare gli eventi e di far sapere agli iscritti corretti quali eventi sono stati attivati. L'aggregatore di eventi non si preoccupa di cosa succede una volta che un evento è stato attivato, il che non è il caso di un mediatore.

Un bell'esempio di mediatore è un tipo di interfaccia guidata. Supponiamo che tu abbia un ampio processo di registrazione per un sistema su cui hai lavorato. Spesso, quando un utente richiede molte informazioni, è buona norma suddividerle in più passaggi.

In questo modo, il codice sarà molto più pulito (più facile da mantenere) e l'utente non sarà sopraffatto dalla quantità di informazioni richieste solo per completare la registrazione. Un mediatore è un oggetto che gestirebbe le fasi di registrazione, tenendo conto dei diversi possibili flussi di lavoro che potrebbero verificarsi a causa del fatto che ogni utente potrebbe potenzialmente avere un processo di registrazione univoco.

L'ovvio vantaggio di questo modello di progettazione è una migliore comunicazione tra le diverse parti di un sistema, che ora comunicano tutte attraverso il mediatore e una base di codice più pulita.

Uno svantaggio sarebbe che ora abbiamo introdotto un singolo punto di errore nel nostro sistema, il che significa che se il nostro mediatore fallisce, l'intero sistema potrebbe smettere di funzionare.

Modello prototipo

Come abbiamo già detto in tutto l'articolo, JavaScript non supporta le classi nella sua forma nativa. L'ereditarietà tra oggetti viene implementata utilizzando la programmazione basata su prototipi.

Ci consente di creare oggetti che possono fungere da prototipo per altri oggetti in fase di creazione. L'oggetto prototipo viene utilizzato come progetto per ogni oggetto creato dal costruttore.

Poiché ne abbiamo già parlato nelle sezioni precedenti, mostriamo un semplice esempio di come potrebbe essere utilizzato questo pattern.

 var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();

Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.

Command Pattern

The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.

This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.

As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.

 // the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));

Facade Pattern

The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.

A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:

 jQuery(".parent .child div.span")

It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.

We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.

Prossimi passi

Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.

In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.

Relazionato: Come sviluppatore JS, questo è ciò che mi tiene sveglio la notte / Dare un senso alla confusione della classe ES6