Comencem el catàleg pel patró que tothom coneix, el que cap en deu línies... i l'únic que bona part de la professió considera avui un antipatró en la majoria dels seus usos. Singleton és la millor lliçó doble del curs: primer aprendràs a implementar-lo bé —que en Java té més suc del que sembla, per culpa de la concurrència—, i després aprendràs per què t'ho hauries de pensar dues vegades abans d'usar-lo. A PideYa el veurem sobre un cas plausible: la configuració global de la plataforma.

Contingut

  1. Intenció i problema a PideYa
  2. Estructura del patró
  3. Implementació clàssica lazy (i el seu bug)
  4. Solucions al problema de concurrència
  5. Variants comparades
  6. Per què és el patró més criticat
  7. L'alternativa: injecció de dependències
  8. Quan usar-lo i quan no
  9. Errors comuns, exercicis i conclusió

Intenció i problema a PideYa

Intenció (GoF): garantir que una classe tingui una sola instància i proporcionar-hi un punt d'accés global.

Fixa't que la intenció té dues meitats —unicitat i accés global— i convé jutjar-les per separat: la primera és sovint legítima; la segona és la font de gairebé tots els problemes.

El cas a PideYa: la plataforma té una configuració global —percentatge de comissió a restaurants, radi màxim de repartiment, URL de la passarel·la de pagament, mode manteniment— que es carrega des d'un fitxer en arrencar. A la lliçó anterior vam veure el símptoma: cada servei feia new ConfiguracioPideYa() i carregava la seva pròpia còpia del fitxer. Conseqüències reals:

  • Cost repetit: el fitxer es parseja un cop per servei (i seria pitjor si la font fos una base de dades remota).
  • Incoherència: l'equip d'operacions va activar el mode manteniment i només el van veure les còpies recarregades; dos serveis van continuar acceptant comandes.

Volem que existeixi una configuració, compartida per tothom. La solució ingènua —una variable global— no existeix com a tal en Java, i encara que existís no impediria que algú continués fent new. Singleton resol totes dues coses fent que la mateixa classe controli la seva instanciació.

Estructura del patró

classDiagram
    class Singleton {
        -static instancia : Singleton
        -Singleton()
        +static getInstancia() Singleton
        +operacio()
    }
    Singleton --> Singleton : crea i desa\nla seva unica instancia

Tres ingredients, tots tres imprescindibles:

Element Paper
Constructor privat Ningú de fora de la classe no pot fer new: la porta principal queda tancada
Atribut estàtic privat amb la instància La classe es desa a si mateixa; és el "magatzem" de la unicitat
Mètode estàtic públic getInstancia() L'únic punt d'accés; crea la instància el primer cop i la reutilitza després

Implementació clàssica lazy (i el seu bug)

La versió de llibre, amb inicialització mandrosa (lazy: la instància no es crea fins que algú la demana per primera vegada):

public class ConfiguracioPideYa {

    private static ConfiguracioPideYa instancia;    // (1) null fins al primer us

    private final Properties valors;

    private ConfiguracioPideYa() {                  // (2) constructor privat
        this.valors = carregarDesDeFitxer("pideya.properties");
    }

    public static ConfiguracioPideYa getInstancia() {   // (3) punt d'acces
        if (instancia == null) {                    // (4) primera vegada?
            instancia = new ConfiguracioPideYa();   // (5) crear i recordar
        }
        return instancia;
    }

    public BigDecimal getComissioRestaurant() {
        return new BigDecimal(valors.getProperty("comision.restaurante"));
    }

    public int getRadiRepartimentKm() {
        return Integer.parseInt(valors.getProperty("reparto.radio.km"));
    }

    private Properties carregarDesDeFitxer(String ruta) { /* ... */ return new Properties(); }
}

Explicació pas a pas:

  • (1) L'atribut estàtic comença a null. En ser estàtic pertany a la classe, no a cap objecte: n'hi ha un de sol per JVM (per class loader, per ser exactes).
  • (2) El constructor privat tanca la porta al new extern. És el que converteix "una convenció" en "una garantia".
  • (4)-(5) La primera crida crea la instància; les següents retornen la ja creada. El cost de carregar el fitxer es paga una vegada i només si algú arriba a necessitar la configuració.

L'ús des de qualsevol punt de PideYa:

BigDecimal comissio = ConfiguracioPideYa.getInstancia().getComissioRestaurant();

El bug: aquesta versió és correcta en un programa d'un sol fil... i PideYa, com qualsevol servidor web, atén moltes peticions concurrents. Si dos fils entren alhora a getInstancia() quan instancia encara és null, tots dos passen l'if del pas (4) i cadascun crea la seva pròpia instància. Resultat: dues configuracions vives, exactament el problema que veníem a resoldre, però ara intermitent i gairebé impossible de reproduir. Aquest és l'exemple canònic de race condition aplicat a la creació; l'estudi general de la concurrència queda per als patrons de concurrència, aquí només necessitem resoldre aquest cas concret.

Solucions al problema de concurrència

Opció A: synchronized al mètode

public static synchronized ConfiguracioPideYa getInstancia() {
    if (instancia == null) {
        instancia = new ConfiguracioPideYa();
    }
    return instancia;
}

synchronized garanteix que només un fil executa el mètode alhora: correcta i simple. La seva pega és que el bloqueig es paga a totes les crides, per sempre, quan només feia falta a la primera. En una classe consultada milers de vegades per segon, és un peatge innecessari (tot i que a la pràctica, amb les JVM modernes, més petit del que la literatura clàssica suggereix).

Opció B: double-checked locking amb volatile

L'intent històric de pagar el bloqueig només la primera vegada:

public class ConfiguracioPideYa {

    private static volatile ConfiguracioPideYa instancia;  // volatile es OBLIGATORI!

    private ConfiguracioPideYa() { /* ... */ }

    public static ConfiguracioPideYa getInstancia() {
        if (instancia == null) {                       // 1a comprovacio: sense bloqueig
            synchronized (ConfiguracioPideYa.class) {
                if (instancia == null) {               // 2a comprovacio: amb bloqueig
                    instancia = new ConfiguracioPideYa();
                }
            }
        }
        return instancia;
    }
}
  • La primera comprovació evita el bloqueig en el 99,99% de crides (la instància ja existeix).
  • La segona comprovació, ja dins del bloc sincronitzat, protegeix el cas en què dos fils van passar la primera alhora: el segon a entrar troba la instància creada i no duplica.
  • volatile no és opcional: sense ell, la JVM pot reordenar la construcció de l'objecte de manera que un altre fil vegi la referència assignada abans que el constructor hagi acabat, rebent un objecte a mig construir. Aquest error va ser tan comú que el double-checked locking va estar trencat en Java fins que el model de memòria de Java 5 va donar a volatile les garanties actuals.

Funciona, però mira quanta subtilesa per inicialitzar un objecte. Per això els dos idiomes següents són preferibles.

Opció C: holder idiom (la recomanada en Java clàssic)

public class ConfiguracioPideYa {

    private ConfiguracioPideYa() { /* ... */ }

    private static class Holder {                    // classe interna: no es carrega
        static final ConfiguracioPideYa INSTANCIA =  // fins que algu la usa
                new ConfiguracioPideYa();
    }

    public static ConfiguracioPideYa getInstancia() {
        return Holder.INSTANCIA;                     // dispara la carrega de Holder
    }
}

La jugada és elegant: la JVM garanteix que la inicialització d'una classe és atòmica i thread-safe (ho assegura el mateix class loader), i que una classe interna estàtica no es carrega fins al seu primer ús. Combinant totes dues coses obtenim lazy + seguretat davant fils sense escriure ni una sola línia de sincronització: la feina bruta la fa la JVM. És l'idiom recomanat quan vols lazy de debò.

Opció D: enum singleton (la d'Effective Java)

public enum RegistreEsdeveniments {

    INSTANCIA;

    private final List<String> esdeveniments = new CopyOnWriteArrayList<>();

    public void registrar(String esdeveniment) {
        esdeveniments.add(Instant.now() + " " + esdeveniment);
    }
}

// Us des de qualsevol punt de PideYa:
RegistreEsdeveniments.INSTANCIA.registrar("Comanda 4412 confirmada");

Aquí el mostrem amb el segon cas clàssic de PideYa: un registre d'esdeveniments/logging simple. Un enum d'un sol valor és, tècnicament, el singleton més robust possible en Java: la JVM garanteix la unicitat, és thread-safe, i a més resisteix dos atacs que trenquen tots els anteriors: la serialització (deserialitzar un singleton normal crea una segona instància si no implementes readResolve()) i la reflexió (amb setAccessible(true) es pot invocar un constructor privat; amb enums, la JVM ho impedeix). Joshua Bloch el recomana a Effective Java com "la millor manera d'implementar un singleton". Els seus límits: no pot estendre una altra classe, no és lazy en sentit estricte (es crea en carregar l'enum) i a molts equips els resulta xocant estilísticament.

Variants comparades

Variant Lazy? Thread-safe? Complexitat Notes
Lazy clàssica Sí No Mínima Només vàlida en contextos monofil; en un servidor, un bug latent
Eager (static final directe) No Sí Mínima Si la instància és barata i sempre s'usa, és perfectament digna
synchronized Sí Sí Baixa Peatge de bloqueig a cada crida
Double-checked + volatile Sí Sí Alta Correcta només des de Java 5 i només amb volatile; fàcil de copiar malament
Holder idiom Sí Sí Baixa La recomanada per a lazy en Java
Enum En carregar l'enum Sí Mínima La més robusta (serialització i reflexió); no pot heretar

Per què és el patró més criticat

Si Singleton fos només "com garantir una instància", seria un idiom innocent. El problema és la seva segona meitat: el punt d'accés global. ConfiguracioPideYa.getInstancia() es pot invocar des de qualsevol línia de qualsevol classe, i això té conseqüències sistèmiques:

  • És estat global amb un altre nom. Tot el que l'enginyeria va aprendre sobre per què les variables globals són nocives (acoblament invisible, efectes a distància, ordre d'inicialització fràgil) s'hi aplica íntegre. Si el singleton a més és mutable (pensa en un setModeManteniment(true)), qualsevol part del programa pot alterar el comportament de qualsevol altra sense que cap signatura ho delati.
  • Amaga les dependències. Mira aquestes dues signatures:
// De que depen aquest servei? Impossible saber-ho sense llegir el cos:
public class ServeiRepartiment {
    public Repartidor assignar(Comanda c) {
        int radi = ConfiguracioPideYa.getInstancia().getRadiRepartimentKm(); // sorpresa!
        // ...
    }
}

// Aqui la dependencia es a la signatura, a la vista:
public class ServeiRepartiment {
    private final ConfiguracioPideYa config;
    public ServeiRepartiment(ConfiguracioPideYa config) { this.config = config; }
}

Amb el singleton, la dependència és un secret del cos del mètode; amb el constructor, és un contracte públic. És la violació pràctica de DIP que vam veure a la lliçó de principis: dependència rígida d'una cosa concreta, i a sobre amagada.

  • Arruïna la testabilitat. El test de ServeiRepartiment amb singleton usa per força la configuració real (i si el test necessita un radi d'1 km? i si un altre test l'ha canviat abans?). L'estat compartit entre tests produeix aquella plaga coneguda: tests que passen sols i fallen en conjunt segons l'ordre d'execució. Amb la dependència injectada, cada test construeix la seva configuració de mentida i en pau.
  • La unicitat sol ser un requisit del desplegament, no de la classe. "Només hi ha una configuració" és cert avui, en aquest procés. El dia que PideYa vulgui tests en paral·lel amb configuracions diferents, o multi-tenant (una configuració per país), la restricció cablejada a la classe es converteix en la paret contra la qual xoques.

L'alternativa: injecció de dependències

La correcció moderna separa les dues meitats de la intenció: mantén la unicitat però elimina l'accés global. Es crea una sola instància a l'arrencada de l'aplicació (l'"arrel de composició", l'únic lloc que sap construir el sistema) i s'injecta a qui la necessiti:

public class Main {
    public static void main(String[] args) {
        // Arrel de composicio: aqui es decideix que existeix i quantes vegades
        ConfiguracioPideYa config = ConfiguracioPideYa.carregarDe("pideya.properties");

        ServeiRepartiment repartiment = new ServeiRepartiment(config);
        ServeiCheckout checkout = new ServeiCheckout(config, repartiment);
        // ...
    }
}

ConfiguracioPideYa deixa de tenir getInstancia(): és una classe normal, testejable, de la qual casualment només es crea una instància perquè el seu creador així ho decideix. La unicitat passa de ser una propietat cablejada a la classe a una decisió de configuració del sistema, que és on pertany. Els contenidors com Spring industrialitzen exactament això: quan declares un bean amb àmbit singleton (l'àmbit per defecte), Spring garanteix una instància única dins del contenidor i la injecta on calgui, sense constructors privats ni estàtics: és el "singleton sense patró Singleton". Com vam veure a la lliçó d'història, aquest és un cas de patró absorbit pels frameworks.

Quan usar-lo i quan no

Usos raonables (pocs i concrets):

  • Recursos tècnics sense estat de negoci, transversals i estables: un registre de logging com RegistreEsdeveniments, caches tècniques, un generador d'identificadors. Com més immutable i més tècnic (lluny del domini), més inofensiu.
  • Aplicacions petites sense contenidor d'injecció, on muntar infraestructura de DI seria sobreenginyeria (lliçó 01-06).
  • Com a detall intern d'implementació d'una altra peça (per exemple, la instància única d'una fàbrica, com veurem a la comparativa).

Evita'l quan:

  • L'objecte té estat de negoci mutable (una CistellaActual singleton és un bug multiusuari esperant a passar).
  • La classe participa en lògica que vols testejar amb dobles: injecta.
  • Treballes amb un framework amb contenidor (Spring, Jakarta EE): usa l'àmbit singleton del contenidor, no el patró.
  • La "unicitat" és en realitat "una per context" (per país, per tenant, per petició): la restricció de classe et farà nosa aviat.

Relació amb altres patrons (només menció): les fàbriques de Factory Method i Abstract Factory s'implementen sovint com a singletons, ja que no solen tenir estat; Facade i els pools de Flyweight també apareixen amb freqüència com a instància única.

Errors Comuns i Consells

  • Copiar el double-checked locking sense volatile. És l'error clàssic d'aquest patró: compila, funciona a les demos, i falla un cop al mes en producció. Si necessites lazy thread-safe, usa el holder idiom i oblida-te'n.
  • El "singleton de conveniència": convertir en singleton qualsevol classe per no haver de passar-la com a paràmetre. Aquest "estalvi" d'un paràmetre es paga amb dependències ocultes i tests fràgils. Passar dependències per constructor no és burocràcia: és documentació executable.
  • Singleton mutable sense sincronitzar. Si la instància és compartida per tots els fils del servidor, el seu estat intern també ho és: o és immutable, o la seva mutabilitat s'ha de protegir (fixa't en el CopyOnWriteArrayList de RegistreEsdeveniments).
  • Oblidar la serialització. Si el teu singleton clàssic implementa Serializable, cada deserialització fabrica una instància nova llevat que defineixis readResolve(). L'enum singleton n'és immune.
  • Consell: abans d'escriure getInstancia(), pregunta't "qui hauria de decidir que això és únic?". Si la resposta és "qui munta l'aplicació" (gairebé sempre ho és), injecta en comptes de globalitzar.

Exercicis

Exercici 1: trobar la fallada

Aquest singleton va aparèixer en una revisió de codi de PideYa. Assenyala tots els problemes:

public class CacheRestaurants {
    private static CacheRestaurants instancia;
    private Map<Long, Restaurant> cache = new HashMap<>();

    public CacheRestaurants() { }

    public static CacheRestaurants getInstancia() {
        if (instancia == null) {
            instancia = new CacheRestaurants();
        }
        return instancia;
    }

    public void desar(Restaurant r) { cache.put(r.getId(), r); }
    public Restaurant cercar(long id) { return cache.get(id); }
}

Exercici 2: reescriure amb holder idiom

Reescriu CacheRestaurants (corregida) usant el holder idiom i una estructura interna segura per a fils.

Exercici 3: del singleton a la injecció

El servei d'assignació de repartidors usa ConfiguracioPideYa.getInstancia() en tres mètodes. Reescriu-lo perquè rebi la configuració per constructor i escriu (en pseudocodi o Java) un test unitari que fixi el radi de repartiment a 2 km sense tocar cap fitxer, comprovant que un repartidor a 5 km no resulta assignable.

Solucions

Solució 1: (a) el constructor és públic: qualsevol pot fer new i trencar la unicitat; (b) la inicialització lazy no és thread-safe: dos fils concurrents poden crear dues caches; (c) HashMap no és segur per a fils i la instància serà compartida per tot el servidor: corrupció o bucles infinits sota càrrega (hauria de ser ConcurrentHashMap); (d) l'atribut cache ni tan sols és final. De propina: una cache d'entitats de negoci com a singleton global mereix la pregunta de la lliçó: no hauria de ser una dependència injectada?

Solució 2:

public class CacheRestaurants {

    private final Map<Long, Restaurant> cache = new ConcurrentHashMap<>();

    private CacheRestaurants() { }

    private static class Holder {
        static final CacheRestaurants INSTANCIA = new CacheRestaurants();
    }

    public static CacheRestaurants getInstancia() {
        return Holder.INSTANCIA;
    }

    public void desar(Restaurant r) { cache.put(r.getId(), r); }
    public Restaurant cercar(long id) { return cache.get(id); }
}

Constructor privat, unicitat i lazy garantides pel class loader, i ConcurrentHashMap per a l'estat compartit.

Solució 3:

public class ServeiAssignacioRepartidors {

    private final ConfiguracioPideYa config;

    public ServeiAssignacioRepartidors(ConfiguracioPideYa config) {
        this.config = config;
    }

    public boolean esAssignable(Repartidor r, Comanda c) {
        return r.distanciaKmFinsA(c.getAdrecaLliurament()) <= config.getRadiRepartimentKm();
    }
}

// Test: sense fitxers, sense estat global, sense ordre d'execucio que importi
@Test
void repartidorForaDeRadiNoEsAssignable() {
    ConfiguracioPideYa config = ConfiguracioPideYa.deValors(Map.of("reparto.radio.km", "2"));
    ServeiAssignacioRepartidors servei = new ServeiAssignacioRepartidors(config);

    Repartidor llunya = repartidorADistanciaKm(5);

    assertFalse(servei.esAssignable(llunya, comandaDeProva()));
}

La clau: en injectar, el test construeix la seva pròpia configuració (aquí amb un mètode de fabricació deValors per a tests) i no depèn de cap estat global. Amb getInstancia() aquest test seria impossible d'aïllar.

Conclusió

Singleton t'ha ensenyat dues coses de valor desigual. La tècnica: en Java, la creació mandrosa i segura davant fils té els seus idiomes (holder per a lazy, enum per a màxima robustesa, i el double-checked locking com a peça de museu que cal saber llegir). I la de disseny, que val per a tota la teva carrera: la intenció del patró barreja una necessitat legítima (unicitat) amb un mecanisme tòxic (accés global), i la pràctica moderna les separa: instància única decidida a l'arrel de composició i injectada com a dependència visible. Quan dubtis, injecta.

El següent patró ataca el problema central de la família amb què vam obrir el mòdul: aquell switch que decideix quin notificador instanciar, repetit per tres serveis de PideYa, trobarà per fi el seu lloc. Ens veiem a Factory Method.

Curs de Patrons de Disseny de Programari

Mòdul 1: Introducció als Patrons de Disseny

Mòdul 2: Patrons Creacionals

Mòdul 3: Patrons Estructurals

Mòdul 4: Patrons de Comportament

Mòdul 5: Aplicació de Patrons de Disseny

Mòdul 6: Patrons de Disseny Avançats

Mòdul 7: Recursos Addicionals i Conclusió

© Copyright 2026. Tots els drets reservats