JavaRush /Blogue Java /Random-PT /Padrões de projeto: Singleton

Padrões de projeto: Singleton

Publicado no grupo Random-PT
Olá! Hoje examinaremos mais de perto os diferentes padrões de design e começaremos com o padrão Singleton, também chamado de “singleton”. Padrões de projeto: Singleton - 1Vamos lembrar: o que sabemos sobre padrões de projeto em geral? Os padrões de projeto são práticas recomendadas que podem ser seguidas para resolver vários problemas conhecidos. Os padrões de design geralmente não estão vinculados a nenhuma linguagem de programação. Tome-os como um conjunto de recomendações, a partir das quais você poderá evitar erros e não reinventar a roda.

O que é um singleton?

Um singleton é um dos padrões de design mais simples que pode ser aplicado a uma classe. Às vezes, as pessoas dizem “esta classe é um singleton”, o que significa que esta classe implementa o padrão de design singleton. Às vezes é necessário escrever uma classe para a qual apenas um objeto possa ser criado. Por exemplo, uma classe responsável por registrar ou conectar-se a um banco de dados. O padrão de design Singleton descreve como podemos realizar tal tarefa. Um singleton é um padrão de design que faz duas coisas:
  1. Fornece uma garantia de que uma classe terá apenas uma instância da classe.

  2. Fornece um ponto de acesso global para uma instância desta classe.

Portanto, existem dois recursos característicos de quase todas as implementações do padrão singleton:
  1. Construtor privado. Restringe a capacidade de criar objetos de classe fora da própria classe.

  2. Um método estático público que retorna uma instância da classe. Este método é chamado getInstance. Este é o ponto de acesso global à instância da classe.

Opções de implementação

O padrão de design singleton é usado de diferentes maneiras. Cada opção é boa e ruim à sua maneira. Aqui, como sempre: não existe ideal, mas é preciso lutar por isso. Mas antes de mais nada, vamos definir o que é bom e o que é ruim, e quais métricas influenciam a avaliação da implementação de um padrão de design. Vamos começar com o positivo. Aqui estão os critérios que conferem suculência e atratividade à implementação:
  • Inicialização lenta: quando uma classe é carregada enquanto o aplicativo está sendo executado exatamente quando é necessário.

  • Simplicidade e transparência do código: a métrica, claro, é subjetiva, mas importante.

  • Segurança de thread: funciona corretamente em um ambiente multithread.

  • Alto desempenho em um ambiente multithread: os threads bloqueiam uns aos outros minimamente ou não bloqueiam nenhum ao compartilhar um recurso.

Agora os contras. Listamos os critérios que mostram a implementação de forma negativa:
  • Inicialização não lenta: quando uma classe é carregada quando a aplicação é iniciada, independentemente de ser necessária ou não (um paradoxo, no mundo da TI é melhor ser preguiçoso)

  • Complexidade e baixa legibilidade do código. A métrica também é subjetiva. Assumiremos que se o sangue sair dos olhos, a implementação é razoável.

  • Falta de segurança do fio. Em outras palavras, “perigo do fio”. Operação incorreta em um ambiente multithread.

  • Baixo desempenho em um ambiente multithread: os threads bloqueiam uns aos outros o tempo todo ou frequentemente ao compartilhar um recurso.

Código

Agora estamos prontos para considerar várias opções de implementação, listando os prós e os contras:

Solução Simples

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
A implementação mais simples. Prós:
  • Simplicidade e transparência do código

  • Segurança do fio

  • Alto desempenho em um ambiente multithread

Desvantagens:
  • Inicialização não preguiçosa.
Na tentativa de corrigir a última falha, obtemos a implementação número dois:

Inicialização lenta

public class Singleton {
  private static Singleton INSTANCE;

  private Singleton() {}

  public static Singleton getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singleton();
    }
    return INSTANCE;
  }
}
Prós:
  • Inicialização lenta.

Desvantagens:
  • Não é seguro para threads

A implementação é interessante. Podemos inicializar lentamente, mas perdemos a segurança do thread. Não tem problema: na implementação número três sincronizamos tudo.

Acessador sincronizado

public class Singleton {
  private static Singleton INSTANCE;

  private Singleton() {
  }

  public static synchronized Singleton getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singleton();
    }
    return INSTANCE;
  }
}
Prós:
  • Inicialização lenta.

  • Segurança do fio

Desvantagens:
  • Baixo desempenho em um ambiente multithread

Ótimo! Na implementação número três, trouxemos de volta a segurança do thread! É verdade, é lento... Agora o método getInstanceestá sincronizado e você só pode inseri-lo um de cada vez. Na verdade, não precisamos sincronizar todo o método, mas apenas aquela parte dele na qual inicializamos um novo objeto de classe. Mas não podemos simplesmente agrupar synchronizeda parte responsável pela criação de um novo objeto em um bloco: isso não fornecerá segurança ao thread. É um pouco mais complicado. O método de sincronização correto é fornecido abaixo:

Bloqueio verificado duas vezes

public class Singleton {
    private static Singleton INSTANCE;

  private Singleton() {
  }

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
Prós:
  • Inicialização lenta.

  • Segurança do fio

  • Alto desempenho em um ambiente multithread

Desvantagens:
  • Não suportado em versões Java inferiores a 1.5 (a palavra-chave volátil foi corrigida na versão 1.5)

Observo que para que esta opção de implementação funcione corretamente, é necessária uma de duas condições. A variável INSTANCEdeve ser final, ou volatile. A última implementação que discutiremos hoje é Class Holder Singleton.

Titular da classe Singleton

public class Singleton {

   private Singleton() {
   }

   private static class SingletonHolder {
       public static final Singleton HOLDER_INSTANCE = new Singleton();
   }

   public static Singleton getInstance() {
       return SingletonHolder.HOLDER_INSTANCE;
   }
}
Prós:
  • Inicialização lenta.

  • Segurança do fio.

  • Alto desempenho em um ambiente multithread.

Desvantagens:
  • Para o correto funcionamento é necessário garantir que o objeto da classe Singletonseja inicializado sem erros. Caso contrário, a primeira chamada de método getInstanceterminará em erro ExceptionInInitializerErrore todas as subsequentes falharão NoClassDefFoundError.

A implementação é quase perfeita. E preguiçoso, seguro para threads e rápido. Mas há uma nuance descrita no sinal negativo. Tabela de comparação de várias implementações do padrão Singleton:
Implementação Inicialização lenta Segurança do fio Velocidade multithread Quando usar?
Solução Simples - + Rápido Nunca. Ou quando a inicialização lenta não é importante. Mas nunca melhor.
Inicialização lenta + - Não aplicável Sempre quando o multithreading não é necessário
Acessador sincronizado + + Devagar Nunca. Ou quando a velocidade de trabalho com multithreading não importa. Mas nunca melhor
Bloqueio verificado duas vezes + + Rápido Em casos raros, quando você precisa lidar com exceções ao criar um singleton. (quando Class Holder Singleton não é aplicável)
Titular da classe Singleton + + Rápido Sempre quando o multithreading é necessário e há uma garantia de que um objeto de classe singleton será criado sem problemas.

Prós e contras do padrão Singleton

Em geral, o singleton faz exatamente o que se espera dele:
  1. Fornece uma garantia de que uma classe terá apenas uma instância da classe.

  2. Fornece um ponto de acesso global para uma instância desta classe.

No entanto, esse padrão tem desvantagens:
  1. Singleton viola o SRP (Princípio de Responsabilidade Única) – a classe Singleton, além de suas responsabilidades imediatas, também controla o número de suas cópias.

  2. A dependência de uma classe ou método regular em um singleton não é visível no contrato público da classe.

  3. Variáveis ​​globais são ruins. O singleton eventualmente se transforma em uma variável global robusta.

  4. A presença de um singleton reduz a testabilidade da aplicação em geral e das classes que utilizam o singleton em particular.

OK, está tudo acabado agora. Vimos o padrão de design singleton. Agora, em uma conversa para a vida toda com seus amigos programadores, você poderá dizer não apenas o que há de bom nisso, mas também algumas palavras sobre o que há de ruim nisso. Boa sorte em dominar novos conhecimentos.

Leitura adicional:

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION