
Il Singleton garantisce che una classe abbia una sola istanza e fornisce un punto di accesso globale a quell'istanza. In JavaScript moderno il modo più semplice è un campo statico privato con un metodo getInstance(): static #instance; static getInstance() { return Database.#instance ??= new Database(); }. Un modulo ES che esporta un oggetto è già un singleton: spesso è la risposta migliore.
Il Singleton è probabilmente il design pattern più conosciuto e più discusso. La sua promessa è semplice: una classe di cui può esistere una sola istanza, accessibile da ovunque. Una connessione a un database, un logger, la configurazione dell’applicazione sono gli esempi classici. In JavaScript si scrive in poche righe, e il linguaggio offre perfino un’alternativa gratuita: il modulo. Ecco le implementazioni che funzionano oggi, con le loro insidie.
Il Singleton in una frase
Un Singleton impedisce la creazione di più istanze di una classe e fornisce un punto di accesso unico all’istanza esistente. La prima chiamata crea l’oggetto; le successive restituiscono lo stesso. Lo si usa quando una risorsa deve davvero essere condivisa: aprire dieci connessioni allo stesso database perché dieci moduli hanno fatto new Database() sarebbe uno spreco, e un log spezzato in dieci file sarebbe inutilizzabile.
Lo si sconsiglia non appena serve soltanto a evitare di passare un parametro: diventa allora una variabile globale travestita, difficile da testare e da sostituire. Ci torniamo a fine articolo.
Implementazione 1: classe con campo statico privato
È la scrittura moderna consigliata. Il campo #instance è privato e statico: appartiene alla classe, non agli oggetti, e nessuno può leggerlo o sovrascriverlo dall’esterno. getInstance() crea l’istanza alla prima chiamata grazie all’operatore ??= («assegna se nullo»).
class Database {
static #instance;
constructor(dsn) {
this.dsn = dsn;
this.connectedAt = new Date();
}
static getInstance(dsn = 'mongodb://localhost') {
Database.#instance ??= new Database(dsn);
return Database.#instance;
}
query(sql) {
return `${this.dsn} > ${sql}`;
}
}
const a = Database.getInstance('mongodb://prod');
const b = Database.getInstance('mysql://autre');
console.log(a === b); // true: stesso oggetto
console.log(b.dsn); // 'mongodb://prod': il secondo dsn è stato ignoratoL’ultima riga mostra un’insidia del pattern: i parametri della seconda chiamata vanno persi in silenzio. Se il tuo singleton prende parametri, o arrivano da una sorgente unica (la configurazione), oppure getInstance() solleva un errore quando gliene passi di diversi.
Nulla impedisce ancora di scrivere new Database() direttamente. JavaScript non ha costruttori privati; se ne simula uno con un token noto solo alla classe:
const cle = Symbol('Database');
class Database {
static #instance;
constructor(jeton) {
if (jeton !== cle) {
throw new Error('Utilisez Database.getInstance()');
}
}
static getInstance() {
return (Database.#instance ??= new Database(cle));
}
}
new Database(); // Error: Utilisez Database.getInstance()Implementazione 2: il costruttore restituisce l’istanza esistente
È la versione storica di questo articolo, e funziona ancora: se un costruttore restituisce esplicitamente un oggetto, new restituisce quell’oggetto invece di quello nuovo. L’istanza viene memorizzata in una proprietà statica.
class Database {
constructor(data) {
if (Database.instance) {
return Database.instance; // new restituisce l'oggetto esistente
}
this._data = data;
Database.instance = this;
}
getData() {
return this._data;
}
setData(data) {
this._data = data;
}
}
const mongo = new Database('mongo');
console.log(mongo.getData()); // 'mongo'
const mysql = new Database('mysql');
console.log(mysql.getData()); // 'mongo': è lo stesso oggetto di mongo
console.log(mongo === mysql); // trueHa il vantaggio di lasciare intatta la sintassi new Database() per il codice chiamante. Ha lo svantaggio di sorprendere: un new che non crea nulla va contro l’intuizione di chi legge il codice, e Database.instance è pubblica, quindi modificabile. La versione con campo privato è più esplicita.
Implementazione 3: un modulo ES è già un singleton
Un modulo JavaScript viene valutato una sola volta per applicazione, qualunque sia il numero di file che lo importano. Esportare un’istanza basta quindi a ottenere un singleton, senza classe né getInstance().
const config = Object.freeze({
env: process.env.NODE_ENV ?? 'development',
apiUrl: 'https://api.example.com',
});
export default config;class Logger {
#lines = [];
log(message) {
this.#lines.push(`${new Date().toISOString()} ${message}`);
}
get count() {
return this.#lines.length;
}
}
export const logger = new Logger(); // una sola istanza per tutta l'applicazioneimport { logger } from './logger.js';
import config from './config.js';
logger.log(`Démarrage en ${config.env}`);Ogni file che importa logger riceve lo stesso oggetto. È la soluzione da preferire nella maggior parte dei progetti: è leggibile, senza magia, e il sistema dei moduli garantisce l’unicità. Due limiti: l’istanza viene creata al caricamento del modulo, anche se nessuno la usa, e il singleton è unico soltanto per percorso di file (due copie di uno stesso pacchetto in node_modules fanno due istanze).
Versione TypeScript
TypeScript aggiunge quello che manca a JavaScript: un costruttore davvero private, che vieta new già in compilazione.
class Database {
private static instance: Database | undefined;
private constructor(private readonly dsn: string) {}
static getInstance(dsn = 'mongodb://localhost'): Database {
Database.instance ??= new Database(dsn);
return Database.instance;
}
query(sql: string): string {
return `${this.dsn} > ${sql}`;
}
}
const db = Database.getInstance();
// new Database('x'); // Errore TS2673: il costruttore è privatoL’equivalente in PHP
La struttura è identica: proprietà statica, costruttore privato, metodo getInstance(). PHP ci aggiunge due lucchetti utili, __clone privato e __wakeup che rifiuta la deserializzazione, perché non possa comparire nessuna copia.
final class Database
{
private static ?Database $instance = null;
private function __construct(private readonly string $dsn)
{
}
public static function getInstance(string $dsn = 'mysql:host=localhost'): Database
{
return self::$instance ??= new self($dsn);
}
public function query(string $sql): string
{
return "{$this->dsn} > {$sql}";
}
private function __clone()
{
}
public function __wakeup(): void
{
throw new LogicException('Un singleton ne se désérialise pas.');
}
}
$a = Database::getInstance('mysql:host=prod');
$b = Database::getInstance();
var_dump($a === $b); // bool(true)
echo $a->query('SELECT 1'); // mysql:host=prod > SELECT 1Quando non usare un Singleton
Il pattern è criticato per buone ragioni, ed è bene conoscerle prima di adottarlo:
- Nasconde le dipendenze. Una funzione che chiama
Database.getInstance()in mezzo al suo codice dipende dal database senza dirlo nella firma. Passare la connessione come parametro (dependency injection) rende la dipendenza visibile e sostituibile. - Complica i test. Lo stato sopravvive da un test all’altro. Se tieni un singleton, aggiungi un metodo
static reset()riservato ai test, oppure testa attraverso l’injection. - È uno stato globale. Tutto quello che vale per le variabili globali vale anche per il singleton: accoppiamento, effetti a distanza, ordine di inizializzazione.
La regola pratica: un modulo che esporta un’istanza (implementazione 3) per la configurazione e le utility senza stato; la dependency injection per tutto ciò che tocca una risorsa esterna; e la classe con getInstance() quando una libreria impone questo punto di accesso unico. Il pattern guard clauses e le classi in JavaScript proseguono il discorso sul versante della struttura del codice.
Errori frequenti
static reset() riservato ai test, oppure inietta la dipendenza invece di chiamare getInstance() nel codice applicativo.new Database('mysql') restituisce l'istanza creata con «mongo»: gli argomenti della seconda chiamata vanno persi senza nessun avviso. Documentalo, oppure solleva un errore se i parametri sono diversi.getInstance().

