Executando verificação de segurança...
2

Design Patterns: Singleton

Singleton Design Pattern

Motivação

Singleton é uma classe a qual somente uma única instância pode exister. O objetivo aqui é garantir que uma classe tenha apenas uma instância e apenas um ponto de acesso global.

Exemplo de Aplicação Prática

classDiagram
    class Singleton {
        - static instance: Singleton
        - Singleton()
        + statc getInstance(): Singleton
    }

Sendo assim a implementação seria algo do tipo:

package br.com.jorgerabellodev.singleton;

public final class Singleton {

    private static Singleton instance;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // teremos apenas uma instância
        }
        return instance;
    }
}

Execução e Uso

package br.com.jorgerabellodev.singleton;

public class Main {
    public static void main(String[] args) {

        Singleton firstSingleton = Singleton.getInstance();
        Singleton secondSingleton = Singleton.getInstance();

        // note que se referem ao mesmo endereço
        System.out.println(firstSingleton);
        System.out.println(secondSingleton);

        // logo serão iguais ao serem comparados com equals()
        System.out.println(firstSingleton.equals(secondSingleton));
        System.out.println(secondSingleton.equals(firstSingleton));
    }
}

Testes Unitários

package br.com.jorgerabellodev.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

@TestMethodOrder(MethodOrderer.DisplayName.class)
class SingletonTest {

    @Test
    @DisplayName("Deve haver apenas uma instância do singleton")
    void deveHaverApenasUmaInstanciaDoSingleton() {
        Singleton firstInstance = Singleton.getInstance();
        Singleton secondInstance = Singleton.getInstance();

        boolean isEquals = firstInstance.equals(secondInstance);

        Assertions.assertThat(isEquals).isTrue();
    }
}

Note também que como o contrutor é privado, fica impossível instanciar um Singleton com new Singleton() por exemplo. A única forma de se conseguir uma instância é por meio do método estático getInstance(), que por sua vez sempre vai checar se o deve ou não criar uma nova instância.

Caso de Uso

Vamos imaginar uma aplicação onde temos uma classe de configurações e esa classe de configurações deve ler as configurações do arquivo config.properties localizado no projeto.

Queremos que haja apenas uma instância de configuração e logo, para tal, vamos implementar o design pattern singleton.

classDiagram
    class Configuration {
        - static configuration: Configuration
        - static properties: Properties
        - Configuration()
        + statc getInstance(): Configuration
    }
graph LR;
    id1(config.properties\n\nclassdriver\nusername\npassword);

Primeiro crie o arquivo config.properties em /src/main/resources/config.properties, com o seguinte conteúdo:

classDriver=jdbc:mysql://localhost/dbname?userUnicode=true&characterEncoding=utf8
username=yourUsername
password=yourSecretPassword

Agora vamos implementar a classe que lê esse arquivo de configurações, utilizando o design pattern Singleton.

package br.com.jorgerabellodev.singleton;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class Configuration {
    private static Configuration configuration;
    private static Properties properties;

    private Configuration() {
        try {
            if (properties == null) {
                properties = new Properties();
                InputStream resourceAsStream = this.getClass().getResourceAsStream("/config.properties");
                properties.load(resourceAsStream);
            }
        } catch (IOException exception) {
            exception.printStackTrace();
        }
    }

    public static Configuration getInstance() {
        if (configuration == null) {
            configuration = new Configuration(); // single instance
        }
        return configuration;
    }

    public static String get(String key) {
        return properties.getProperty(key);
    }
}

Exemplos de Possíveis Testes Unitários

package br.com.jorgerabellodev.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

@TestMethodOrder(MethodOrderer.DisplayName.class)
class ConfigurationTest {

    @Test
    @DisplayName("Deve haver apenas uma instância do singleton de configuração")
    void deveHaverApenasUmaInstanciaDoSingleton() {

        Configuration firstInstance = Configuration.getInstance();
        Configuration secondInstance = Configuration.getInstance();

        boolean isEquals = firstInstance.equals(secondInstance);

        Assertions.assertThat(isEquals).isTrue();
    }

    @Test
    @DisplayName("Dado que as instâncias são iguais devem sempre recuperar os mesmos dados")
    void dadoQueAsInstanciasSaoIguaisDevemSempreRecuperarOsMesmosDados() {
        Configuration firstInstance = Configuration.getInstance();
        Configuration secondInstance = Configuration.getInstance();

        String firstClassDriver = firstInstance.get("classDriver");
        String firstUserName = firstInstance.get("username");
        String firstPassword = firstInstance.get("password");

        String secondClassDriver = secondInstance.get("classDriver");
        String secondUserName = secondInstance.get("username");
        String secondPassword = secondInstance.get("password");

        Assertions.assertThat(firstClassDriver).isEqualTo(secondClassDriver);
        Assertions.assertThat(firstUserName).isEqualTo(secondUserName);
        Assertions.assertThat(firstPassword).isEqualTo(secondPassword);
    }
}

Código

https://bitbucket.org/jorge_rabello/singleton/src/master/

1
1
1

Bom post, mas acho que vale complementar com alguns detalhes.

A implementação sugerida não é thread-safe, ou seja, se várias threads chamarem Singleton.getInstance(), pode ser que ele crie mais de uma instância. Modifiquei um pouco o seu exemplo para ilustrar melhor:

public final class Singleton {
    private static Singleton instance;
    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) { // adicionando uns prints pra saber quando cria uma instância
            System.out.println("criando nova instancia: ");
            instance = new Singleton();
            System.out.println("nova instancia criada: " + instance);
        }
        return instance;
    }
}

E testando com várias threads:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class Teste {
    public static void main(String[] args) throws Exception {
        List<Callable<Singleton>> tasks = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            tasks.add(() -> Singleton.getInstance());
        }
        ExecutorService executor = Executors.newFixedThreadPool(10);
        for (Future<Singleton> result : executor.invokeAll(tasks)) {
            System.out.println(result.get());
        }
        executor.awaitTermination(3, TimeUnit.SECONDS);
        executor.shutdown();
    }
}

Ou seja, crio 10 threads, e todas elas chamam Singleton.getInstance(). No final eu percorro os resultados e vejo qual instância foi retornada. O resultado foi:

criando nova instancia: 
criando nova instancia: 
criando nova instancia: 
nova instancia criada: Singleton@47a2a66c
nova instancia criada: Singleton@6f9777db
nova instancia criada: Singleton@1dc022f6
Singleton@47a2a66c
Singleton@47a2a66c
Singleton@47a2a66c
Singleton@6f9777db
Singleton@47a2a66c
Singleton@47a2a66c
Singleton@47a2a66c
Singleton@47a2a66c
Singleton@47a2a66c
Singleton@47a2a66c

No caso, foram criadas 3 instâncias. Claro que isso pode variar a cada execução, já que threads não são determinísticas, mas enfim, se precisar de um singleton em um ambiente multi-thread, aí tem que mudar um pouco a abordagem.


Alternativa thread-safe

Uma forma de resolver é usar a técnica chamada Initialization-on-demand holder, explicada em detalhes aqui. Ficaria assim:

public final class Singleton {
    private Singleton() {
        System.out.println("criando novo singleton"); // apenas para fins de debug
    }

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

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

Agora rodando o mesmo código que roda as 10 threads, o resultado é:

criando novo singleton
Singleton@7ba4f24f
Singleton@7ba4f24f
Singleton@7ba4f24f
Singleton@7ba4f24f
Singleton@7ba4f24f
Singleton@7ba4f24f
Singleton@7ba4f24f
Singleton@7ba4f24f
Singleton@7ba4f24f
Singleton@7ba4f24f

Ou seja, apenas uma instância é criada. Basicamente, isso é garantido pela forma como o class loader trabalha. A explicação abaixo foi retirada do link já citado:

  • o class loader carrega as classes assim que elas são acessadas (neste caso, o único acesso a Holder é dentro do método getInstance())
  • quando uma classe é carregada, e antes que qualquer um a use, é garantido que todos os inicializadores estáticos são executados (é aí que o campo Holder.INSTANCE é inicializado)
  • o class loader tem seus próprios mecanismos de sincronização, que garantem que o código acima é thread safe

Indo um pouco mais além, vale lembrar que um singleton é único por JVM. Ou seja, se seu ambiente roda em um cluster, cada nó do cluster terá uma instância. Se quer garantir unicidade entre os clusters, aí precisa de outras técnicas específicas, e varia muito caso a caso (depende do servidor de aplicação, do que exatamente vc está fazendo com o singleton, etc).

1

Uaaaaalllll kht que top demais !!!

Muito obrigado primeiro pelo conhecimento compartilhado e aprendizado (aprendi mais uma hj), segundo segundo pelo complemento ao post, valeu demais ^^.