Già in passato ci è capitato di trattare il pattern Singleton, in Javascript (ES5+NodeJS). Vediamo oggi come implementarlo con il nuovo Javascript EcmaScript 6 (ES6).

I singleton per definizione sono quei moduli che non han bisogno di scrivere sul global space e che non han senso di esistere in più istanze.
Con ES6 si introducono nuovi costrutti e sintassi per esportare e importare nativamente moduli.

Il pattern

Creiamo una classe ed esportiamone un’istanza:

// File: yolo.js
 
class Yolo {}
export let yolo = new Yolo();

Per importarla, lo facciamo da un altro modulo. Sarà ES6 ad assicurarsi che l’istanza importata sia la stessa istanza importata anche altrove.

// File: laser.js
 
import { yolo } from "./yolo.js";
// yolo is a single instance of Yolo class
// File: another-file.js
 
import { yolo } from "./yolo.js";
// same yolo as in laster.js

Esempio

In questo esempio mostriamo:

1) Un modulo principale che contiene un altro modulo figlio.
2) Entrambi i moduli condividono la stessa istanza di un broker di notifica.

Per eseguire questo specifico esempio si richiedono le seguenti dipendenze:

$ npm install webpack babel-loader babel-preset-es2015

Classe di notifica

Creiamo una classe basilare in un file chiamato notifications.js. Il costruttore crea un array che conterrà i messaggi, mostrati successivamente.
Il secondo metodo chiamato add(message) inserisce un dato messaggio all’interno del nostro array.

// notifications.js
export class Notifications {
 
  constructor() {
    this.messages = [];
  }
 
  add(message) {
    this.messages.push(message);
  }
}

Per motivi di debug aggiungiamo anche un document.write ed un console.log al metodo add:

add(message) {
  this.messages.push(message);
 
  // debug
  document.write(`<p>${this.messages.length} - ${message}</p>`);
  console.log('messages', this.messages);
}

Ogni volte che qualcuno chiama il metodo add otteniamo il numero di messaggi presenti nell’istanza della nostra classe.

E ci basta importare così la classe:

import { Notifications } from "./notifications.js“;

Oltre ad esportare la classe, stiamo esportando una nuova istanza della classe. Usando “let” si esporterà sempre una e una sola istanza della classe.

export let notifications = new Notifications();

Questa è tutta la magia che ci sta dietro. Let usato nel global scope assicura che notifications non venga riusato. Così che ogni volta che facciamo import { notifications } from "./notifications.js"; otteniamo la stessa istanza della classe Notifications.

Child-module

Il primo utilizzo che andiamo a fare della nostra classe di notifica è quello di importarla in un modulo-figlio. Quando questo modulo figlio viene istanziato aggiunge in automatico un messaggio nella classe di notifica:

// child.js
import { notifications } from "./notifications.js";
 
export class Child {
 
  constructor(name) {
    this.name = name;
 
    notifications.add('yolo from ' + this.name)
  }
 
}

Main-Module

Mettiamo adesso tutto insieme creando un file chiamato main.js che rappresenta il flusso d’esecuzione principale della nostra applicazione:

// main.js
import { notifications } from "./notifications.js";
import { Child } from "./child.js";
 
export class Main {
  constructor() {
    notifications.add('yolo 1 from main');
 
    // create new children
    // (they call notifications.add in constructor)
    let child1 = new Child('le child 1');
    let child2 = new Child('le child 2');
 
    // send second message from main
    notifications.add('yolo 2 from main');
  }
}

Infine eseguiamo quando il DOM della nostra pagina web è stato caricato:

document.addEventListener("DOMContentLoaded", (e) => new Main());

Eseguiamo

Quando webPack esegue il tutto nel browser nel document.body ci ritroveremo:

<p>1 - yolo 1 from main</p>
<p>2 - yolo from le child 1</p>
<p>3 - yolo from le child 2</p>
<p>4 - yolo 2 from main</p>

E’ proprio la numerazione a fianco che ci dimostra come l’istanza della classe “Notifications” sia unica.

Estendere la classe

// transformer.js
import { Yolo } from "./yolo.js";
// Yolo is Yolo class
 
class TransformerYolo extends Yolo {}
 
export let transformerYolo = new TransformerYolo();

Esportare la classe “rompe” il pattern di singleton?!? Ebbene sì:

Singleton pattern restricts object creation for a class to only one instance.

Quindi se fate: import { Notifications, notifications } from "./notifications.js“; e poi new Notifications();, otterrete un’altra istanza della classe Notifications. Non si hanno restrizioni dal creare altre istanze della classe. Se invece si vuol ottenere il pattern singleton vero e proprio allora vi conviene NON esportare la classe. 🙂

Potete trovare un l’esempio funzionante descritto sopra su webpackbin.com