
O Singleton garante que uma classe só tem uma instância e fornece um ponto de acesso global a essa instância. Em JavaScript moderno, a forma mais simples é um campo estático privado com um método getInstance(): static #instance; static getInstance() { return Database.#instance ??= new Database(); }. Um módulo ES que exporta um objeto já é um singleton: muitas vezes é a melhor resposta.
O Singleton é provavelmente o design pattern mais conhecido e mais discutido. A promessa é simples: uma classe da qual só pode existir uma instância, acessível a partir de qualquer sítio. Uma ligação a uma base de dados, um logger ou a configuração da aplicação são os exemplos clássicos. Em JavaScript, escreve-se em poucas linhas, e a linguagem oferece até uma alternativa gratuita: o módulo. Aqui ficam as implementações que funcionam hoje, com as suas armadilhas.
O Singleton numa frase
Um Singleton impede a criação de várias instâncias de uma classe e fornece um ponto de acesso único à instância existente. A primeira chamada cria o objeto; as seguintes devolvem o mesmo. Usa-se quando um recurso tem mesmo de ser partilhado: abrir dez ligações à mesma base de dados porque dez módulos fizeram new Database() seria um desperdício, e um log espalhado por dez ficheiros seria inutilizável.
Desaconselha-se assim que serve apenas para evitar passar um parâmetro: torna-se então uma variável global disfarçada, difícil de testar e de substituir. Voltamos a isto no fim do artigo.
Implementação 1: classe com campo estático privado
É a escrita moderna recomendada. O campo #instance é privado e estático: pertence à classe, não aos objetos, e ninguém o consegue ler ou substituir a partir de fora. O getInstance() cria a instância na primeira chamada, graças ao operador ??= («atribui se for nulo»).
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 — o mesmo objeto
console.log(b.dsn); // 'mongodb://prod' — o segundo dsn foi ignoradoA última linha ilustra uma armadilha do pattern: os parâmetros da segunda chamada perdem-se em silêncio. Se o teu singleton receber parâmetros, ou eles vêm de uma fonte única (a configuração), ou o getInstance() lança um erro quando lhe passam parâmetros diferentes.
Nada impede ainda de escrever new Database() diretamente. O JavaScript não tem construtor privado; simula-se com um token conhecido apenas pela 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()Implementação 2: o construtor devolve a instância existente
É a versão histórica deste artigo e continua a funcionar: se um construtor devolve explicitamente um objeto, o new devolve esse objeto em vez do novo. A instância fica guardada numa propriedade estática.
class Database {
constructor(data) {
if (Database.instance) {
return Database.instance; // o new devolve o objeto existente
}
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' — é o mesmo objeto que mongo
console.log(mongo === mysql); // trueTem a vantagem de deixar a sintaxe new Database() intacta para quem a chama. Tem o inconveniente de surpreender: um new que não cria nada vai contra a intuição de quem lê o código, e Database.instance é público, logo modificável. A versão com campo privado é mais explícita.
Implementação 3: um módulo ES já é um singleton
Um módulo JavaScript só é avaliado uma vez por aplicação, seja qual for o número de ficheiros que o importam. Exportar uma instância chega, portanto, para obter um singleton, sem classe nem 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(); // uma única instância para toda a aplicaçãoimport { logger } from './logger.js';
import config from './config.js';
logger.log(`Démarrage en ${config.env}`);Qualquer ficheiro que importe logger recebe o mesmo objeto. É a solução a preferir na maioria dos projetos: é legível, não tem magia e o sistema de módulos garante a unicidade. Duas limitações: a instância é criada no carregamento do módulo, mesmo que ninguém a use, e o singleton só é único por caminho de ficheiro (duas cópias de um pacote em node_modules dão duas instâncias).
Versão TypeScript
O TypeScript acrescenta o que falta ao JavaScript: um construtor realmente private, que proíbe o new na compilação.
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'); // Erro TS2673: o construtor é privadoO equivalente em PHP
A estrutura é idêntica: propriedade estática, construtor privado, método getInstance(). O PHP acrescenta dois trincos úteis, o __clone privado e o __wakeup que recusa a desserialização, para que nenhuma cópia possa aparecer.
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 não usar um Singleton
O pattern é criticado por boas razões, e vale a pena conhecê-las antes de o adotar:
- Esconde as dependências. Uma função que chama
Database.getInstance()a meio do código depende da base de dados sem o dizer na assinatura. Passar a ligação como parâmetro (injeção de dependências) torna a dependência visível e substituível. - Complica os testes. O estado sobrevive de um teste para o outro. Se ficares com um singleton, acrescenta um método
static reset()reservado aos testes, ou testa através da injeção. - É estado global. Tudo o que é verdade para as variáveis globais é verdade para o singleton: acoplamento, efeitos à distância, ordem de inicialização.
A regra prática: um módulo que exporta uma instância (implementação 3) para a configuração e para os utilitários sem estado; a injeção de dependências para tudo o que toca num recurso externo; e a classe com getInstance() quando uma biblioteca impõe esse ponto de acesso único. O pattern guard clauses e as classes em JavaScript prolongam este tema do lado da estrutura do código.
Erros frequentes
static reset() reservado aos testes, ou injeta a dependência em vez de chamar getInstance() no código de negócio.new Database('mysql') devolve a instância criada com «mongo»: os argumentos da segunda chamada perdem-se sem aviso. Documenta-o, ou lança um erro se os parâmetros forem diferentes.getInstance().

