Padrão Singleton em JavaScript: classe ES6, módulo e TypeScript

O pattern Singleton garante uma instância única. Três implementações JavaScript executadas (classe com campo estático privado, construtor que devolve a instância, módulo ES), a versão TypeScript, o equivalente em PHP e os casos em que é melhor dispensá-lo.

Design pattern Singleton em JavaScript com classes ES6
Resposta rápida

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»).

database.js
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 ignorado

A ú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:

javascript
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.

database-constructeur.js
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); // true

Tem 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().

config.js
const config = Object.freeze({
  env: process.env.NODE_ENV ?? 'development',
  apiUrl: 'https://api.example.com',
});

export default config;
logger.js
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ção
app.js
import { 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.

database.ts
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 é privado

O 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.

Database.php
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 1

Quando 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

Uma instância impossível de reiniciar nos testes Um singleton guarda o seu estado de um teste para o outro. Prevê um método static reset() reservado aos testes, ou injeta a dependência em vez de chamar getInstance() no código de negócio.
Parâmetros ignorados na segunda chamada 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.
Um singleton por cada cópia do módulo Duas versões do mesmo pacote instaladas em node_modules dão dois módulos, logo dois «singletons». O módulo ES só é único para um dado caminho de ficheiro.
Código executado no import Uma instância criada no carregamento do módulo é construída mesmo que ninguém a use. Prefere a instanciação preguiçosa dentro do getInstance().

JavaScript

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.