Pular para o conteúdo

Singleton é um padrão de projeto que garante que uma classe tenha apenas uma instância e fornece um ponto global de acesso a ela. Ele é útil quando é necessário controlar o acesso a recursos compartilhados, como conexões de banco de dados, arquivos de configuração ou serviços de log.

Em muitos sistemas, existem recursos que deveriam ser únicos na aplicação inteira:

  • Um arquivo de configuração central.
  • Um gerenciador de conexão com o banco de dados.
  • Um serviço de log que registra tudo que acontece no sistema.

Se cada parte do sistema puder criar sua própria instância dessas classes, alguns problemas surgem:

  • Inconsistência de dados: duas instâncias de configuração podem ter valores diferentes carregados em momentos distintos.
  • Uso excessivo de recursos: criar várias conexões de banco pode estourar o limite de conexões do servidor.
  • Dificuldade de coordenação: se existem vários objetos que “mandam” na mesma coisa, fica difícil saber qual deles está com o estado correto.
  • Dependências espalhadas: cada módulo sabe como criar sua própria instância, espalhando lógica de criação por todo o código.

Precisamos de uma forma de garantir que só exista uma instância de determinadas classes e que todos os pontos do sistema usem exatamente essa mesma instância.

O Singleton propõe três ideias principais:

  1. Construtor privado
    Impede que outras partes do código criem objetos diretamente com new.
    Assim, a própria classe controla quando e como a instância é criada.

  2. Atributo estático para armazenar a instância única
    A classe mantém um atributo estático (por exemplo, instancia) que guarda o único objeto criado.

  3. Método estático de acesso (geralmente getInstancia)

    • Se a instância ainda não existe, o método a cria e a armazena no atributo estático.
    • Se a instância já existe, o método simplesmente a retorna.
    • Todo o sistema passa a usar esse método como ponto único de acesso.

Com isso:

  • A classe mesma controla sua própria quantidade de instâncias.
  • O código cliente não se preocupa em “quantas” instâncias existem, apenas chama MeuSingleton.getInstancia().

Pense em um serviço de emergência (como o 190, 192, 193, etc.):

  • Você pode ligar de diferentes telefones: fixo, celular, telefone público.
  • Pode discar números diferentes dependendo do tipo de emergência.
  • Independentemente de como você ligue, a ligação vai parar em um serviço central de emergência que coordena o atendimento.

O importante é:

  • Existem vários canais de acesso, mas o serviço que responde é único e centralizado.
  • Você não quer que cada telefone crie seu próprio “mini serviço de emergência” local. Isso geraria confusão, dados incompletos, falta de coordenação, etc.

O Singleton funciona de forma parecida:

  • O “serviço de emergência” é a instância única.
  • Os “diferentes telefones” são as várias partes do sistema acessando o mesmo serviço.
  • O número de telefone (por exemplo getInstancia()) é o ponto global de acesso ao serviço.

Use Singleton quando:

  • Deve existir apenas uma instância de uma classe na aplicação:
    • Gerenciador de configurações da aplicação.
    • Serviço de log central.
    • Gerenciador de conexões com banco de dados.
    • Gerenciador de fila de mensagens / eventos.
  • Você precisa de um ponto global de acesso a um recurso compartilhado:
    • Em uma aplicação desktop: preferência do usuário, tema atual, idioma atual.
    • Em uma aplicação web (back-end): cache em memória, pool de conexões, provedor de autenticação.
  • Você quer ter mais controle sobre a inicialização tardia (lazy):
    • Só criar o objeto quando ele realmente for necessário, economizando recursos durante o startup.

Mas lembre-se: “usar Singleton” não significa apenas “quero algo global”; significa que você precisa garantir unicidade + acesso controlado.

Apesar de útil, Singleton é frequentemente considerado um anti-padrão quando usado de forma exagerada:

  1. Dificulta testes unitários

    • Singletons normalmente são globais e estáticos.
    • Isso dificulta substituir o objeto por mocks ou stubs em testes.
    • Testes começam a depender de estado global, o que gera testes “flaky” (que às vezes passam, às vezes falham).

    Analogia: testar um sistema que usa um serviço de emergência “global” e real é como tentar simular um incêndio de verdade para ver se os bombeiros chegam. Seria melhor ter um “serviço falso para testes”.

  2. Acoplamento global

    • Qualquer classe pode chamar diretamente Singleton.getInstancia().
    • Seu código passa a depender diretamente do Singleton, o que torna mudanças futuras mais difíceis.
    • É parecido com usar variáveis globais: fácil no começo, caro para manter depois.

    Analogia: é como se todos na cidade tivessem o número pessoal do chefe do serviço de emergência e ligassem direto para o celular dele, ignorando a central. No início parece prático, mas vira caos rapidamente.

  3. Uso desnecessário quando múltiplas instâncias não causariam problema

    • Às vezes, ter mais de uma instância é totalmente aceitável ou até desejável (por exemplo, múltiplas conexões isoladas de banco em testes).
    • Forçar Singleton só porque “fica mais simples chamar estático” acaba limitando o design.
  4. Concorrência e estado compartilhado

    • Em ambientes multithread, vários threads podem tentar acessar e alterar o mesmo Singleton.
    • Sem o devido cuidado, isso leva a condições de corrida e bugs difíceis de reproduzir.

De forma geral: use Singleton com cuidado. Em muitos casos, injeção de dependência ou passar explicitamente as dependências por construtor/método é uma solução mais flexível.

Um passo a passo de uma implementação simples em Java:

  1. Tornar o construtor privado

    public class ConfiguracaoSistema {
    private ConfiguracaoSistema() {
    // construtor privado impede new ConfiguracaoSistema() fora da classe
    }
    }
  2. Criar um atributo estático privado para guardar a instância

    public class ConfiguracaoSistema {
    private static ConfiguracaoSistema instancia;
    private ConfiguracaoSistema() {
    // inicialização
    }
    }
  3. Criar um método estático público que retorna a instância

    public class ConfiguracaoSistema {
    private static ConfiguracaoSistema instancia;
    private ConfiguracaoSistema() {
    // carregaria configurações de um arquivo, por exemplo
    }
    public static ConfiguracaoSistema getInstancia() {
    if (instancia == null) {
    instancia = new ConfiguracaoSistema();
    }
    return instancia;
    }
    }
  4. Utilizar o Singleton no código cliente

    public class ExemploUso {
    public static void main(String[] args) {
    ConfiguracaoSistema cfg1 = ConfiguracaoSistema.getInstancia();
    ConfiguracaoSistema cfg2 = ConfiguracaoSistema.getInstancia();
    System.out.println(cfg1 == cfg2); // true, mesma instância
    }
    }
  5. Considerar questões de concorrência (multithread)
    A implementação acima não é segura em ambientes multithread. Em cenários concorrentes, usar:

    • Inicialização estática antecipada, ou
    • Enum, ou
    • Técnicas de sincronização (synchronized, double-checked locking), se realmente necessário.
  • Controle explícito sobre a quantidade de instâncias

    • Garante que só existe uma instância de uma classe (ex.: serviço de configuração).
  • Ponto global de acesso

    • Qualquer parte do sistema obtém o mesmo objeto via getInstancia().
    • Facilita acesso a recursos compartilhados (log, config, cache).
  • Inicialização tardia (lazy)

    • Pode adiar a criação do objeto até ele realmente ser necessário.
    • Útil quando a criação é custosa (ex.: carregar configurações de arquivo grande).
  • Substitui variáveis globais por uma solução mais estruturada

    • Pelo menos, a criação e o acesso são encapsulados dentro da classe.
  • Dificulta testes

    • Estado global e estático torna testes menos previsíveis.
    • É difícil isolar testes e usar mocks.
  • Acoplamento forte ao Singleton

    • Muitas classes passam a depender diretamente de uma implementação concreta.
    • Trocar a implementação ou extrair uma interface depois é mais trabalhoso.
  • Risco de virar “lixeira global”

    • É tentador colocar “tudo que precisa ser global” dentro do Singleton.
    • Isso aumenta o tamanho e a responsabilidade da classe, violando o SRP (princípio da responsabilidade única).
  • Problemas em ambientes multithread

    • Implementações ingênuas não são thread-safe.
    • Correções para torná-las thread-safe podem introduzir complexidade (locks, sincronização).
  • Factory Method / Abstract Factory

    • Podem ser usados dentro de um Singleton para criar objetos relacionados (por exemplo, um FabricaDeRepositorios Singleton).
    • O Singleton controla “quantas fábricas existem”, enquanto a fábrica controla “como objetos são criados”.
  • Dependency Injection (DI)

    • Muitas vezes, DI é uma alternativa melhor a Singletons.
    • Em vez de chamar MeuSingleton.getInstancia(), você recebe a dependência por construtor ou injeção.
    • Isso reduz acoplamento, facilita testes e permite trocar implementações.
  • Facade

    • Um Facade pode ser implementado como Singleton, quando você quer uma única fachada para um subsistema.
    • Porém, Facade é sobre expor uma interface simplificada; Singleton é sobre unicidade da instância.
  • State / Strategy

    • Às vezes, cada estado/estratégia pode ser um Singleton se o estado não guarda dados específicos por contexto (por exemplo, estados imutáveis únicos).

No lazy singleton, a instância é criada apenas na primeira vez que é solicitada.

public class ServicoEmergencia {
private static ServicoEmergencia instancia;
private ServicoEmergencia() {
// configura o serviço (por exemplo, carrega regras, contatos, etc.)
}
public static ServicoEmergencia getInstancia() {
if (instancia == null) {
instancia = new ServicoEmergencia();
}
return instancia;
}
public void atenderLigacao(String tipoEmergencia) {
System.out.println("Atendendo emergência: " + tipoEmergencia);
}
}

Uso:

public class AppUniversitaria {
public static void main(String[] args) {
ServicoEmergencia servico1 = ServicoEmergencia.getInstancia();
ServicoEmergencia servico2 = ServicoEmergencia.getInstancia();
servico1.atenderLigacao("incendio no laboratorio");
System.out.println(servico1 == servico2); // true
}
}

Em Java, uma forma simples e segura de implementar Singleton é usando enum:

public enum CentralEmergencia {
INSTANCIA; // única instância
public void atenderLigacao(String tipoEmergencia) {
System.out.println("Central de Emergência atendendo: " + tipoEmergencia);
}
}

Uso:

public class AppUniversitaria {
public static void main(String[] args) {
CentralEmergencia central1 = CentralEmergencia.INSTANCIA;
CentralEmergencia central2 = CentralEmergencia.INSTANCIA;
central1.atenderLigacao("acidente no estacionamento");
System.out.println(central1 == central2); // true
}
}

Essa abordagem com enum é thread-safe, simples e lida bem com serialização, sendo recomendada pelo próprio Joshua Bloch (autor de Effective Java).

Abaixo um exemplo completo usando o tema de configuração de sistema em um projeto de Engenharia de Software (por exemplo, um sistema web usado pela universidade):

public class ConfiguracaoSistema {
private static ConfiguracaoSistema instancia;
private String urlBanco;
private String usuarioBanco;
private String idiomaInterface;
private ConfiguracaoSistema() {
// Em um cenário real, leria de um arquivo .properties, .yaml, etc.
this.urlBanco = "jdbc:mysql://localhost:3306/universidade";
this.usuarioBanco = "aluno_dev";
this.idiomaInterface = "pt-BR";
}
public static ConfiguracaoSistema getInstancia() {
if (instancia == null) {
instancia = new ConfiguracaoSistema();
}
return instancia;
}
public String getUrlBanco() {
return urlBanco;
}
public String getUsuarioBanco() {
return usuarioBanco;
}
public String getIdiomaInterface() {
return idiomaInterface;
}
}

Uso em outra parte do sistema:

public class RepositorioAluno {
public void conectar() {
ConfiguracaoSistema cfg = ConfiguracaoSistema.getInstancia();
System.out.println("Conectando ao banco: " + cfg.getUrlBanco());
// aqui iria o código real de conexão usando a url e o usuário
}
}
public class App {
public static void main(String[] args) {
RepositorioAluno repo1 = new RepositorioAluno();
RepositorioAluno repo2 = new RepositorioAluno();
repo1.conectar();
repo2.conectar();
// Ambos usam a mesma instância de ConfiguracaoSistema,
// assim como diferentes telefones usam o mesmo serviço de emergência central.
}
}