Meilleures pratiques du modèle de conception Singleton en Java avec des exemples

Introduction

Le modèle Singleton Java est l’un des Design patterns du Gang des Quatre et appartient à la catégorie des Design Patterns Créationnels. De par sa définition, il semble s’agir d’un modèle de conception simple, mais lorsqu’il s’agit de sa mise en œuvre, il soulève de nombreuses préoccupations.

Dans cet article, nous allons découvrir les principes du modèle de conception singleton, explorer différentes façons de le mettre en œuvre, et certaines des meilleures pratiques pour son utilisation.

Principes du modèle Singleton

  • Le modèle Singleton restreint l’instanciation d’une classe et garantit qu’une seule instance de la classe existe dans la machine virtuelle Java.
  • La classe singleton doit fournir un point d’accès global pour obtenir l’instance de la classe.
  • Le modèle Singleton est utilisé pour le journalisation, les objets de pilotes, la mise en cache, et le pool de threads.
  • Le modèle de conception Singleton est également utilisé dans d’autres modèles de conception comme Factory Abstraite, Builder, Prototype, Facade, etc.
  • Le modèle de conception Singleton est également utilisé dans les classes principales de Java (par exemple, java.lang.Runtime, java.awt.Desktop).

Implémentation du modèle Singleton en Java

Pour implémenter un modèle singleton, nous avons différentes approches, mais toutes ont les concepts communs suivants.

  • Constructeur privé pour restreindre l’instanciation de la classe à partir d’autres classes.
  • Variable statique privée de la même classe qui est la seule instance de la classe.
  • Méthode statique publique qui renvoie l’instance de la classe, c’est le point d’accès global pour le monde extérieur afin d’obtenir l’instance de la classe singleton.

Dans les sections suivantes, nous apprendrons différentes approches pour l’implémentation du modèle singleton et les préoccupations de conception liées à l’implémentation.

1. Initialisation impatiente

En initialisation impatiente, l’instance de la classe singleton est créée au moment du chargement de la classe. L’inconvénient de l’initialisation impatiente est que la méthode est créée même si l’application cliente ne l’utilise peut-être pas. Voici l’implémentation de la classe singleton avec une initialisation statique :

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // constructeur privé pour éviter que les applications clientes n'utilisent le constructeur
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance() {
        return instance;
    }
}

Si votre classe singleton n’utilise pas beaucoup de ressources, c’est l’approche à adopter. Mais dans la plupart des scénarios, les classes singleton sont créées pour des ressources telles que le système de fichiers, les connexions de base de données, etc. Nous devrions éviter l’instanciation à moins que le client appelle la méthode getInstance. De plus, cette méthode ne fournit aucune option pour la gestion des exceptions.

2. Initialisation par bloc statique

Bloc statique L’implémentation de l’initialisation est similaire à l’initialisation anticipée, sauf que l’instance de la classe est créée dans le bloc statique, offrant ainsi l’option de gestion des exceptions.

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // Initialisation du bloc statique pour la gestion des exceptions
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

    public static StaticBlockSingleton getInstance() {
        return instance;
    }
}

À la fois l’initialisation anticipée et l’initialisation du bloc statique créent l’instance avant même qu’elle ne soit utilisée, ce qui n’est pas la meilleure pratique à suivre.

3. Initialisation Paresseuse

La méthode d’initialisation paresseuse pour implémenter le modèle singleton crée l’instance dans la méthode d’accès globale. Voici le code d’exemple pour créer la classe singleton avec cette approche:

package com.journaldev.singleton;

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

    public static LazyInitializedSingleton getInstance() {
        if (instance == null) {
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

L’implémentation précédente fonctionne bien dans le cas d’un environnement à un seul thread, mais en ce qui concerne les systèmes multi-thread, cela peut poser problème si plusieurs threads se trouvent à l’intérieur de la condition if en même temps. Cela détruira le modèle singleton et les deux threads obtiendront des instances différentes de la classe singleton. Dans la prochaine section, nous verrons différentes façons de créer une classe singleton thread-safe.

4. Singleton Thread-Safe

A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:

package com.journaldev.singleton;

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }

}

L’implémentation précédente fonctionne bien et assure la sécurité des threads, mais elle réduit les performances en raison du coût associé à la méthode synchronisée, bien que nous en ayons besoin uniquement pour les premiers threads qui pourraient créer des instances distinctes. Pour éviter cette surcharge supplémentaire à chaque fois, le principe du verrouillage à double vérification (double-checked locking) est utilisé. Dans cette approche, le bloc synchronisé est utilisé à l’intérieur de la condition if avec une vérification supplémentaire pour garantir qu’une seule instance d’une classe singleton est créée. Le code suivant illustre l’implémentation du verrouillage à double vérification :

public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
    if (instance == null) {
        synchronized (ThreadSafeSingleton.class) {
            if (instance == null) {
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

Poursuivez votre apprentissage avec Thread Safe Singleton Class.

5. Implémentation Singleton Bill Pugh

Avant Java 5, le modèle de mémoire Java présentait de nombreux problèmes, et les approches précédentes échouaient dans certains scénarios où trop de threads tentaient d’obtenir simultanément l’instance de la classe singleton. Ainsi, Bill Pugh a proposé une approche différente pour créer la classe singleton en utilisant une classe d’aide interne statique. Voici un exemple de l’implémentation du Singleton de Bill Pugh :

package com.journaldev.singleton;

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

Remarquez la classe interne privée et statique qui contient l’instance de la classe singleton. Lorsque la classe singleton est chargée, la classe SingletonHelper n’est pas chargée en mémoire, et elle est chargée uniquement lorsqu’un appel à la méthode getInstance() est effectué. Cette approche est la plus largement utilisée pour la classe singleton car elle ne nécessite pas de synchronisation.

6. Utilisation de la réflexion pour détruire le motif Singleton

La réflexion peut être utilisée pour détruire toutes les approches précédentes d’implémentation du singleton. Voici un exemple de classe :

package com.journaldev.singleton;

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                // Ce code détruira le motif singleton
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

Quand vous exécutez la classe de test précédente, vous remarquerez que le hashCode des deux instances n’est pas le même, ce qui détruit le modèle singleton. La réflexion est très puissante et utilisée dans de nombreux frameworks comme Spring et Hibernate. Continuez votre apprentissage avec Tutoriel sur la Réflexion en Java.

7. Singleton Enum

Pour surmonter cette situation avec la réflexion, Joshua Bloch suggère l’utilisation de enum pour implémenter le modèle de conception singleton car Java garantit que toute valeur enum est instanciée une seule fois dans un programme Java. Comme les valeurs Enum Java sont accessibles globalement, le singleton l’est aussi. L’inconvénient est que le type enum est quelque peu inflexible (par exemple, il n’autorise pas l’initialisation paresseuse).

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // faire quelque chose
    }
}

8. Sérialisation et Singleton

Parfois dans les systèmes distribués, nous devons implémenter l’interface Serializable dans la classe singleton afin de pouvoir stocker son état dans le système de fichiers et le récupérer ultérieurement. Voici une petite classe singleton qui implémente également l’interface Serializable :

package com.journaldev.singleton;

import java.io.Serializable;

public class SerializedSingleton implements Serializable {

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}

    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }

}

Le problème avec une classe singleton sérialisée est que chaque fois que nous la désérialisons, elle créera une nouvelle instance de la classe. Voici un exemple :

package com.journaldev.singleton;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        // désérialiser du fichier vers un objet
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());

    }

}

Ce code produit cette sortie :

Output
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522

Il détruit ainsi le modèle singleton. Pour surmonter ce scénario, tout ce que nous devons faire est de fournir l’implémentation de la méthode readResolve().

protected Object readResolve() {
    return getInstance();
}

Après cela, vous remarquerez que le hashCode des deux instances est le même dans le programme de test.

Lisez à propos de la Sérialisation Java et de la Désérialisation Java.

Conclusion

Cet article a couvert le modèle de conception singleton.

Poursuivez votre apprentissage avec plus de tutoriels Java.

Source:
https://www.digitalocean.com/community/tutorials/java-singleton-design-pattern-best-practices-examples