Block Image

Scopo del pattern

Assicurare che una classe abbia un'unica istanza e fornire un unico punto di accesso a tale istanza.

Perché usare il Singleton pattern

Abbiamo la necessità di assicurare che una classe abbia una sola istanza. Ad esempio potremmo avere più stampanti ma vogliamo sfruttare una sola coda di stampa. Oppure all'interno di un sistema operativo, vorremmo poter utilizzare un solo Window Manager. Un caso d'uso nell'ambito della programmazione Web, potrebbe essere la creazione di una classe DAO (Data Access Object); è una buona idea creare una sola istanza per una classe di questo tipo.

Struttura

Come si potrebbe intuire, questo pattern fa parte della categoria dei pattern creazionali. Scriviamo una classe Singleton in Java:

public class Singleton {
    
    private static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    
    public void singletonOperation() {
      System.out.println("I am a Singleton class");
    }
}

Da notare due aspetti che caratterizzano questo pattern:

  1. Il costruttore è privato: non permettiamo al client di creare una classe Singleton con new. La classe Singleton ha la responsabilità di creare la sua unica istanza.

  2. Il metodo getInstance è un metodo di classe (in Java si usa la keyword static) che assicura la creazione dell'unica istanza che sarà disponibile ai client, inizializzando la variabile instance la prima volta che verrà richiamato il metodo: questo approccio viene definito lazy initialization.

  3. I client, per utilizzare la classe Singleton, devono per forza chiamare il metodo getInstance, che fornisce quindi un unico punto di accesso all'istanza.

Gestione di un Singleton Thread Safe

Con l'implementazione precedente, la creazione del Singleton in getInstance, non è thread safe. Per renderla tale, dobbiamo assicurare che in un ambiente multithread, la creazioni dell'istanza venga fatta una sola volta (lockiamo il controllo se la variabile instance è null, utilizzando in Java la keyword synchronized). Uno dei modi per farlo è il seguente:

private static volatile Singleton instance;
...
public static Singleton getInstance() {
    if(instance == null) {
        synchronized (Singleton.class) {
            if(instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

Da notare che il controllo viene effettuato due volte.
Questo per non rischiare che un thread rimasto in attesa dal blocco synchronized possa sovrascrivere instance già inizializzato da un thread precedente.
Inoltre, in Java possiamo sfruttare la funzionalità della keyword volatile, che permette l'accesso in lettura di una variabile da parte di un thread solo dopo essere terminata la fase di scrittura su quest'ultima.

Un'alternativa più semplice è quella di rendere l'intero metodo getInstance thread safe (keyword synchronized a livello del metodo), ma questo rende assai inefficiente la fase di lettura del Singleton.

Un'altra alternativa è quella di inizializzare instance allo startup:

public class Singleton {

    private static final Singleton INSTANCE = new Singleton();

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

In questo modo però non utilizziamo la lazy initialization, istanziamo il Singleton anche nel caso in cui non verrà mai utilizzato.

Ci sono poi altre alternative, come dichiarare il Singleton come Enum, oppure delegare l'inizializzazione del Singleton a una inner class della stessa.

Creare sottoclassi di un Singleton

Avendo il costruttore privato, non solo non diamo ai client la possibilità di istanziare il Singleton, ma anche di non poter definire delle sottoclassi: questo è il normale comportamento di un Singleton.

È possibile però usare dei workaround per raggiungere questo scopo.

Possiamo ad esempio creare un registro di Singleton, e il client, tramite una variabile di ambiente, decide quale implementazione utilizzare. La classe Singleton non deve avere la responsabilità di conoscere le sue sottoclassi, le basta recuperare dal registro la sottoclasse che l'utente vuole utilizzare, in base alla variabile d'ambiente. Vediamo come fare:

public abstract class Singleton {

    private static Singleton instance;

    protected static Map<String, Singleton> registry = new HashMap<>();

    protected Singleton() {}

    public static Singleton getInstance() {
        if(instance == null) {
            String singletonName = System.getenv("singleton");
            instance = registry.get(singletonName);
            if(instance == null) {
                throw new RuntimeException("No Singleton implementation found!");
            }
        }
        return instance;
    }

    public abstract void singletonOperation();

}

Notiamo che il Singleton ha una variabile registry che è una mappa chiave-valore, con la chiave che rappresenta il nome di una implementazione del Singleton e il valore che rappresenta la sua corrispondente istanza.

Notiamo anche che il costruttore, ovviamente, non è più privato, ma protetto (in Java keyword protected), in modo tale da essere richiamabile almeno all'interno delle possibili sottoclassi.

Un esempio di implementazione potrebbe essere:

class SubSingletonA extends Singleton {

    static {
        registry.put("A", new SubSingletonA());
    }

    private SubSingletonA() { }

    @Override
    public void singletonOperation() {
        System.out.println("I am the SubSingletonA class");
    }

}

La sottoclasse avrà l'unica responsabilità di aggiungere nel registro la propria istanza.

Conclusioni

Abbiamo visto che cos'è il pattern Singleton, uno dei pattern creazionali più utlizzati.
Abbiamo anche visto che questo pattern, nella sua forma naturale, dà dei vincoli al programmatore. Nella fase di test ad esempio, non è possibile mockare una classe di questo genere.

Se possibile, è da preferire l'uso di bean (ad esempio tramite Spring) con scope singleton piuttosto che usare una classe Singleton, in quanto i bean sono più potenti; assicurano che ci sia un'unica istanza ma permettono ad esempio un'agevole fase di test in quanto possono essere mockati facilmente.