Padrão de Criação (GoF)

Singleton

Garante que uma classe tenha exatamente uma instância durante toda a vida da aplicação e fornece um ponto de acesso global a essa instância.

Intenção

Assegurar que uma classe seja instanciada uma única vez e prover um ponto de acesso global a essa instância, eliminando a necessidade de passar a referência por toda a aplicação.

O Singleton é um dos 23 padrões do catálogo original de Gamma, Helm, Johnson e Vlissides — o livro Design Patterns: Elements of Reusable Object-Oriented Software (1994), carinhosamente chamado de "Gang of Four" (GoF). Pertence à categoria de padrões de criação, que lidam com mecanismos de instanciação de objetos.

Problema

Algumas responsabilidades em um sistema precisam existir em um único lugar: o pool de conexões com o banco de dados, o logger central, o cache em memória, as configurações carregadas de um arquivo, o barramento de eventos. Criar múltiplas instâncias dessas estruturas causa inconsistências: cada instância mantém seu próprio estado, os logs são fragmentados, o cache é duplicado.

A solução ingênua é usar uma variável global. O problema: variáveis globais permitem que qualquer parte do código sobrescreva o valor, não garantem que a inicialização ocorra uma única vez e tornam as dependências implícitas, dificultando testes e manutenção.

O Singleton resolve isso encapsulando a lógica de controle de instância dentro da própria classe, tornando impossível (ou ao menos explicitamente difícil) criar uma segunda instância acidentalmente.

Solução

A implementação canônica do Singleton tem três elementos obrigatórios:

  1. Construtor privado (ou protegido): impede que código externo instancie a classe com new.
  2. Campo estático privado: armazena a única instância da classe (o campo começa null ou undefined).
  3. Método estático público (getInstance()): verifica se a instância existe; se não existir, cria e armazena; sempre retorna a mesma referência.

Ao chamar MinhaClasse.getInstance() várias vezes, todas as chamadas recebem o mesmo objeto — não há cópias, não há estado duplicado.

Estrutura

O diagrama abaixo, em notação UML simplificada, mostra a classe Singleton com seus membros essenciais:

┌───────────────────────────────────────────┐
│                 <<Singleton>>              │
│               ConfigManager               │
├───────────────────────────────────────────┤
│ - instance: ConfigManager  (estático)     │
│ - dados: Record<string, string>           │
├───────────────────────────────────────────┤
│ - constructor()            (privado)      │
│ + getInstance(): ConfigManager  (estático)│
│ + get(chave: string): string | undefined  │
│ + set(chave: string, valor: string): void │
└───────────────────────────────────────────┘

Fluxo de uso:

  Código cliente A                Código cliente B
       │                               │
       │  ConfigManager.getInstance()  │  ConfigManager.getInstance()
       │ ─────────────────────────────►│
       │                               │
       │         ┌─────────────────────┤
       │         │  instância já existe│
       │         │  → retorna a mesma  │
       │         └─────────────────────┤
       │◄──────────────────────────────│
       │        mesma referência       │

Exemplos de código

Exemplo 1 — Implementação ingênua (não thread-safe)

Esta é a forma mais simples do Singleton. Em JavaScript e PHP (single-threaded na maioria dos casos), funciona bem para entender o padrão. Mais adiante discutimos as limitações.

class Logger {
  // Campo estático privado que guarda a única instância.
  private static instance: Logger | null = null;

  // Construtor privado — impede `new Logger()` fora da classe.
  private constructor(private readonly prefix: string = "[LOG]") {}

  // Ponto de acesso global: cria na primeira chamada, reutiliza nas demais.
  public static getInstance(): Logger {
    if (Logger.instance === null) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  public info(mensagem: string): void {
    console.log(`${this.prefix} [INFO] ${mensagem}`);
  }

  public erro(mensagem: string): void {
    console.error(`${this.prefix} [ERRO] ${mensagem}`);
  }
}

// ── Uso ──────────────────────────────────────────────────────
const logA = Logger.getInstance();
const logB = Logger.getInstance();

console.log(logA === logB); // true — mesma referência

logA.info("Aplicação iniciada");
logB.erro("Algo deu errado");
// Ambas as chamadas escrevem no mesmo logger.

Exemplo 2 — Singleton com inicialização lazy e tipagem forte

Em aplicações reais, o Singleton costuma encapsular um recurso inicializado com parâmetros (como uma string de conexão). O padrão abaixo aceita opções na primeira chamada e as ignora nas chamadas subsequentes.

interface ConfigOpcoes {
  readonly arquivo: string;
  readonly ambiente: "dev" | "staging" | "prod";
}

class ConfigManager {
  private static instance: ConfigManager | null = null;
  private readonly dados: Map<string, string> = new Map();

  private constructor(opcoes: ConfigOpcoes) {
    // Simula o carregamento de um arquivo de configuração.
    this.dados.set("env",     opcoes.ambiente);
    this.dados.set("arquivo", opcoes.arquivo);
    console.log(`[Config] Carregado de "${opcoes.arquivo}" (${opcoes.ambiente})`);
  }

  // Na primeira chamada, `opcoes` é obrigatório.
  // Nas chamadas subsequentes, o argumento é ignorado.
  public static getInstance(opcoes?: ConfigOpcoes): ConfigManager {
    if (ConfigManager.instance === null) {
      if (!opcoes) {
        throw new Error("ConfigManager: opcoes obrigatórias na primeira inicialização.");
      }
      ConfigManager.instance = new ConfigManager(opcoes);
    }
    return ConfigManager.instance;
  }

  public get(chave: string): string | undefined {
    return this.dados.get(chave);
  }

  public set(chave: string, valor: string): void {
    this.dados.set(chave, valor);
  }

  /** Apenas para testes — permite resetar a instância entre casos de teste. */
  public static resetParaTestes(): void {
    ConfigManager.instance = null;
  }
}

// ── Uso ──────────────────────────────────────────────────────
const cfg = ConfigManager.getInstance({
  arquivo: ".env.prod",
  ambiente: "prod",
});

console.log(cfg.get("env")); // "prod"

// Segunda chamada sem opções — retorna a instância existente.
const cfg2 = ConfigManager.getInstance();
console.log(cfg === cfg2);   // true

Quando usar

  • Recurso compartilhado com estado único: pool de conexões com banco de dados, cliente HTTP com configuração centralizada, cache em memória.
  • Logger / sistema de auditoria: toda a aplicação deve escrever no mesmo destino, sem fragmentação de saída.
  • Configuração carregada uma vez: variáveis de ambiente, feature flags, parâmetros de aplicação carregados na inicialização.
  • Barramento de eventos (event bus) simples: um único canal que distribui eventos entre subsistemas.

Quando evitar

  • Objetos de domínio: Pedido, Usuario, Produto nunca devem ser Singletons — cada entidade tem identidade própria.
  • Services em frameworks com DI: NestJS, Spring, Laravel e similares já gerenciam o ciclo de vida dos serviços. Use o escopo do container (singleton scope no DI) em vez de implementar o padrão manualmente.
  • Quando a testabilidade importa: Singletons com estado persistente entre testes quebram o isolamento. Se não houver um método de reset (como o resetParaTestes() do exemplo), prefira injeção de dependência.

Prós e contras

Prós

  • Garante uma única instância — sem estado duplicado nem inconsistência.
  • Ponto de acesso global consistente, sem necessidade de passar referências por parâmetros.
  • Inicialização lazy (a instância só é criada quando solicitada pela primeira vez), economizando recursos se o Singleton nunca for usado.
  • Fácil de localizar no código: basta buscar por getInstance().

Contras

  • Viola o Princípio da Responsabilidade Única: a classe gerencia tanto sua lógica de negócio quanto seu ciclo de vida.
  • Dificulta testes unitários — é preciso um mecanismo de reset ou usar técnicas de mock avançadas.
  • Introduz acoplamento global implícito: qualquer parte do código pode acessar e modificar o estado do Singleton.
  • Problemas de concorrência em ambientes multi-thread (Java, C#, Go) — a verificação check-then-act não é atômica sem sincronização.
  • Dificulta a detecção de dependências: ao contrário da injeção de dependência, o acoplamento com o Singleton não aparece na assinatura do construtor.

Armadilhas comuns

1. Estado global disfarçado

O Singleton é, na prática, uma variável global orientada a objetos. Qualquer parte do sistema pode chamar Singleton.getInstance() e alterar seu estado. Isso cria dependências ocultas que não aparecem nas assinaturas dos métodos e que só se revelam em tempo de execução — especialmente doloroso em projetos grandes com múltiplas equipes.

2. Dificulta testes e mocks

Atenção: Testes unitários que dependem de um Singleton com estado podem interferir entre si se executados na mesma instância de processo. O estado do Singleton do Teste A "vaza" para o Teste B.

A solução clássica é adicionar um método resetParaTestes() (como no Exemplo 2) ou, melhor ainda, refatorar o código para aceitar a dependência via injeção (construtor ou parâmetro) — aí nos testes você passa um objeto fake sem precisar do Singleton.

3. Problemas de concorrência (multi-thread) e ciclo de vida

Em TypeScript/Node.js e PHP padrão (sem pthreads), a execução é single-threaded e a condição de corrida não ocorre. Mas em Java, C# ou Go, dois threads podem passar pela verificação if (instance == null) simultaneamente e criar duas instâncias. A solução é usar sincronização (synchronized, lock, ou inicialização estática garantida pela linguagem).

Atenção — PHP vs Node.js: há uma diferença de ciclo de vida que confunde muita gente. O PHP padrão segue o modelo share-nothing por request: cada requisição HTTP inicia um processo (ou worker) do zero, executa o script e descarta tudo ao final. Isso significa que o Singleton não persiste entre requisições — ele é recriado a cada request. Não há vazamento de estado entre usuários, mas também não há ganho de inicialização entre chamadas. Já em TypeScript/Node.js, o processo é long-lived: o mesmo módulo é carregado uma vez e permanece em memória enquanto o servidor estiver de pé. O Singleton criado na primeira requisição sobrevive e é reutilizado por todas as requisições subsequentes — o que traz os benefícios de cache e os riscos de estado compartilhado entre usuários concorrentes.

4. Herança e subclasses

Singleton e herança não combinam bem. Se LoggerFilho extends Logger, o campo estático instance pode ser compartilhado entre pai e filho dependendo da linguagem, gerando comportamentos inesperados. Com construtor private (como nos exemplos acima), a herança é simplesmente impossível — o que torna desnecessário, e enganoso, o uso de static::$instance (late static binding) em PHP. Se você precisar de um Singleton herdável, troque o construtor para protected e aí sim adote static::$instance com new static() — mas esteja ciente de que a complexidade aumenta rapidamente.

5. Serialização e desserialização

Em PHP, se o objeto for serializado e desserializado (ex.: via unserialize()), uma nova instância é criada, quebrando a garantia do Singleton. A solução é implementar __wakeup() lançando uma exceção para proibir a desserialização.

Padrões relacionados

O Singleton frequentemente aparece em conjunto ou em contraste com outros padrões:

O Monostate é uma alternativa ao Singleton: em vez de controlar a instância, todos os campos são estáticos, de modo que múltiplos objetos compartilham o mesmo estado — mais fácil de testar, mas igualmente acoplado. O Flyweight também controla a criação de instâncias, mas para compartilhar objetos imutáveis em grande número. O Facade frequentemente é implementado como Singleton para fornecer um ponto de acesso simplificado a um subsistema.