Si el mòdul s'hagués de reduir a un sol patró, seria aquest: Factory Method és la resposta més directa a la pregunta que va obrir el mòdul —qui decideix quina classe concreta s'instancia?— i la porta d'entrada a tota la mentalitat creacional. En aquesta lliçó el construirem sobre el sistema de notificacions de PideYa (push, SMS, email), el distingirem del seu cosí popular però no catalogat (la simple factory), i acabarem amb les variants modernes que els genèrics i les lambdes de Java han portat al patró.

Contingut

  1. El problema a PideYa
  2. Primer pas: la simple factory (que no és un patró GoF)
  3. El patró Factory Method: estructura GoF
  4. Implementació Java completa
  5. Variants modernes: genèrics i lambdes
  6. Quan usar-lo i quan no
  7. Relació amb altres patrons
  8. Errors comuns, exercicis i conclusió

El problema a PideYa

Recupera el símptoma de la introducció del mòdul: PideYa notifica els seus clients pel canal que cadascú prefereix, i la decisió de quin notificador crear està repetida, amb aquest aspecte, al servei de comandes, al de repartiment i al de promocions:

// Repetit (amb petites variacions que ja divergeixen) en TRES serveis:
Notificador n;
switch (client.getCanalPreferit()) {
    case PUSH:  n = new NotificadorPush(); break;
    case SMS:   n = new NotificadorSms(clauTwilio); break;
    case EMAIL: n = new NotificadorEmail(servidorSmtp); break;
    default: throw new IllegalArgumentException();
}
n.enviar(client, missatge);

Els dolors concrets:

  • Duplicació (violació de DRY): afegir el canal WhatsApp, que negoci acaba de demanar, exigeix localitzar i editar els tres switch. En un s'oblidarà.
  • Violació d'OCP: cada canal nou modifica codi estable en diversos llocs.
  • Coneixement de construcció dispers: la clau de Twilio i el servidor SMTP apareixen allà on es copiï el bloc.

La bona notícia: el codi client ja programa contra la interfície Notificador per usar l'objecte. Només falla en el moment de crear-lo. Necessitem extreure aquest moment a un lloc únic i extensible.

Primer pas: la simple factory (que no és un patró GoF)

El primer reflex de qualsevol desenvolupador és moure el switch a una classe amb un mètode estàtic:

public class FabricaNotificadors {

    public static Notificador crear(CanalNotificacio canal) {
        return switch (canal) {
            case PUSH  -> new NotificadorPush();
            case SMS   -> new NotificadorSms(ConfigMissatgeria.clauTwilio());
            case EMAIL -> new NotificadorEmail(ConfigMissatgeria.servidorSmtp());
        };
    }
}

// Als tres serveis, el bloc sencer es redueix a:
Notificador n = FabricaNotificadors.crear(client.getCanalPreferit());

Això s'anomena simple factory (o static factory) i convé dir-ho alt i clar: no és un patró del catàleg GoF; és un idiom, un honest pas de refactorització. I no el menyspreïs: ja ha resolt dos dels tres dolors (la duplicació desapareix i el coneixement de construcció es centralitza). Per a moltíssims casos reals, aquí pots parar, i fer-ho és un acte de sana antisobreenginyeria.

El que la simple factory no resol: el switch continua existint, en un únic lloc però tancat. Afegir WhatsApp continua sent modificar la fàbrica (OCP ferit, tot i que ara el dany és local), i sobretot: ningú no pot estendre el sistema de canals sense tocar el codi central —un problema seriós si, per exemple, els mòduls de PideYa els mantenen equips diferents, o si el nucli de notificacions es distribueix com a llibreria a les apps de restaurants—. Quan aquest eix d'extensió és real, entra el patró de debò.

El patró Factory Method: estructura GoF

Intenció (GoF): definir una interfície per crear un objecte, deixant que les subclasses decideixin quina classe instanciar. Factory Method permet a una classe delegar la instanciació en les seves subclasses.

La idea-força: el codi general (el creador) conté tota la lògica de negoci que usa el producte, però deixa un forat —un mètode de fabricació, el factory method— que cada subclasse omple decidint el producte concret. És OCP en estat pur: per afegir un canal, s'afegeix una parella de classes; no es modifica res d'existent. I és l'únic creacional d'àmbit de classe: la variació s'aconsegueix heretant.

classDiagram
    class ServeiNotificacions {
        <<abstract>>
        +notificarCanviComanda(Comanda)
        #crearNotificador()* Notificador
    }
    class ServeiNotificacionsPush {
        #crearNotificador() Notificador
    }
    class ServeiNotificacionsSms {
        #crearNotificador() Notificador
    }
    class Notificador {
        <<interface>>
        +enviar(Client, String)
    }
    class NotificadorPush {
        +enviar(Client, String)
    }
    class NotificadorSms {
        +enviar(Client, String)
    }
    ServeiNotificacions <|-- ServeiNotificacionsPush
    ServeiNotificacions <|-- ServeiNotificacionsSms
    Notificador <|.. NotificadorPush
    Notificador <|.. NotificadorSms
    ServeiNotificacions ..> Notificador : usa el que crea
    ServeiNotificacionsPush ..> NotificadorPush : crea
    ServeiNotificacionsSms ..> NotificadorSms : crea

Els quatre rols GoF, amb la seva encarnació a PideYa:

Rol GoF Paper A PideYa
Product Interfície del que es fabrica Notificador
ConcreteProduct Implementacions concretes NotificadorPush, NotificadorSms, NotificadorEmail
Creator Classe (sovint abstracta) amb la lògica comuna; declara el factory method ServeiNotificacions
ConcreteCreator Subclasse que implementa el factory method i tria el producte ServeiNotificacionsPush, ServeiNotificacionsSms, ...

Implementació Java completa

El producte i les seves implementacions (sense canvis respecte del que PideYa ja tenia):

public interface Notificador {
    void enviar(Client client, String missatge);
}

public class NotificadorPush implements Notificador {
    @Override
    public void enviar(Client client, String missatge) {
        // Enviament mitjancant el servei de push a l'app del client
        System.out.println("[PUSH a " + client.getIdDispositiu() + "] " + missatge);
    }
}

public class NotificadorSms implements Notificador {
    private final String clauTwilio;

    public NotificadorSms(String clauTwilio) { this.clauTwilio = clauTwilio; }

    @Override
    public void enviar(Client client, String missatge) {
        System.out.println("[SMS a " + client.getTelefon() + "] " + missatge);
    }
}

public class NotificadorEmail implements Notificador {
    private final String servidorSmtp;

    public NotificadorEmail(String servidorSmtp) { this.servidorSmtp = servidorSmtp; }

    @Override
    public void enviar(Client client, String missatge) {
        System.out.println("[EMAIL a " + client.getEmail() + "] " + missatge);
    }
}

El creador: fixa't que conté lògica de negoci real (compondre el missatge, decidir quan notificar, registrar l'enviament) que és comuna a tots els canals. El patró brilla justament per això: no és "una classe que només fabrica", és una classe amb feina pròpia que delega només la decisió d'instanciació:

public abstract class ServeiNotificacions {

    /** Logica de negoci comuna: igual per a tots els canals. */
    public void notificarCanviComanda(Comanda comanda) {
        String missatge = compondreMissatge(comanda);      // feina comuna
        Notificador notificador = crearNotificador();      // <-- EL factory method
        notificador.enviar(comanda.getClient(), missatge); // us del producte
        RegistreEsdeveniments.INSTANCIA.registrar(         // feina comuna
                "Notificada comanda " + comanda.getId());
    }

    private String compondreMissatge(Comanda comanda) {
        return "La teva comanda de " + comanda.getRestaurant().getNom()
                + " esta " + comanda.getEstat().enTextAmigable();
    }

    /** El forat que cada subclasse omple: quin notificador concret usar. */
    protected abstract Notificador crearNotificador();
}

Els creadors concrets, trivials a propòsit —tota la seva raó de ser és una línia—:

public class ServeiNotificacionsPush extends ServeiNotificacions {
    @Override
    protected Notificador crearNotificador() {
        return new NotificadorPush();
    }
}

public class ServeiNotificacionsSms extends ServeiNotificacions {
    private final String clauTwilio;

    public ServeiNotificacionsSms(String clauTwilio) { this.clauTwilio = clauTwilio; }

    @Override
    protected Notificador crearNotificador() {
        return new NotificadorSms(clauTwilio);   // el detall de construccio viu aqui
    }
}

El client tria (o rep injectat, millor) el servei adequat i treballa sempre contra la classe base:

ServeiNotificacions servei = new ServeiNotificacionsSms(config.getClauTwilio());
servei.notificarCanviComanda(comanda);   // sense saber ni importar-li el canal

Què hem comprat? Afegir WhatsApp ara és crear NotificadorWhatsApp + ServeiNotificacionsWhatsApp: dues classes noves, zero modificacions. El switch ha desaparegut del mapa (queda, com a molt, un únic punt a l'arrel de composició que tria quin ServeiNotificacions construir per client, i fins i tot aquest punt es pot eliminar amb el registre que veurem a les variants). I als tests, un ServeiNotificacionsDeProva el factory method del qual retorna un notificador espia permet verificar la lògica comuna sense enviar res de real.

Una variant habitual del creador: donar al factory method una implementació per defecte (per exemple, return new NotificadorPush()) en comptes de deixar-lo abstracte. Així el creador és usable tal qual i les subclasses només existeixen per canviar el producte. També és comú el factory method parametritzat, que rep un argument i tria entre diversos productes: a mig camí cap a la simple factory.

Variants modernes: genèrics i lambdes

Amb genèrics: el creador coneix el tipus exacte

Quan el creador ha de retornar el tipus concret sense obligar el client a fer casts:

public abstract class ServeiNotificacions<T extends Notificador> {
    protected abstract T crearNotificador();
}

public class ServeiNotificacionsPush extends ServeiNotificacions<NotificadorPush> {
    @Override
    protected NotificadorPush crearNotificador() { return new NotificadorPush(); }
}

Java permet a més la covariància del tipus de retorn sense genèrics: una subclasse pot estrènyer el retorn de Notificador a NotificadorPush directament. Els genèrics aporten quan el tipus del producte viatja per més mètodes de la classe.

Amb lambdes: el factory method com a objecte

Des de Java 8, "un mètode que crea un Notificador" té nom propi: Supplier<Notificador> (o Function<X, Notificador> si la creació necessita dades). Això permet una versió composicional del patró: en comptes d'una subclasse per producte, el creador rep la funció de fabricació:

public class ServeiNotificacions {   // ja no es abstracta ni te subclasses!

    private final Supplier<Notificador> fabricaNotificador;

    public ServeiNotificacions(Supplier<Notificador> fabricaNotificador) {
        this.fabricaNotificador = fabricaNotificador;
    }

    public void notificarCanviComanda(Comanda comanda) {
        Notificador n = fabricaNotificador.get();     // el "forat", ara injectat
        n.enviar(comanda.getClient(), compondreMissatge(comanda));
    }
    // ...
}

// Configuracio a l'arrel de composicio: cada "subclasse" es ara una linia
var perPush = new ServeiNotificacions(NotificadorPush::new);
var perSms  = new ServeiNotificacions(() -> new NotificadorSms(config.getClauTwilio()));

Observa el desplaçament conceptual: el patró GoF varia per herència (àmbit de classe); la variant amb lambdes varia per composició (s'injecta la fàbrica), alineant-se amb la màxima "composició per sobre d'herència". En Java modern aquesta forma és freqüentíssima, i és la mateixa idea que explota el registre de fàbriques, que converteix el switch residual en un mapa obert a extensió:

public class RegistreNotificadors {

    private final Map<CanalNotificacio, Supplier<Notificador>> fabriques =
            new EnumMap<>(CanalNotificacio.class);

    public void registrar(CanalNotificacio canal, Supplier<Notificador> fabrica) {
        fabriques.put(canal, fabrica);
    }

    public Notificador crear(CanalNotificacio canal) {
        Supplier<Notificador> fabrica = fabriques.get(canal);
        if (fabrica == null) throw new IllegalArgumentException("Canal sense registrar: " + canal);
        return fabrica.get();
    }
}

// Arrencada de PideYa: registrar es AFEGIR una linia, no editar un switch
registre.registrar(CanalNotificacio.PUSH,  NotificadorPush::new);
registre.registrar(CanalNotificacio.SMS,   () -> new NotificadorSms(config.getClauTwilio()));
registre.registrar(CanalNotificacio.EMAIL, () -> new NotificadorEmail(config.getServidorSmtp()));

Cada mòdul de PideYa (fins i tot un plugin de tercers) pot registrar els seus canals sense que el nucli els conegui. El JDK és ple d'aquesta filosofia: els mètodes estàtics de fabricació com List.of(...), Optional.of(...) o Files.newBufferedReader(...) són parents de la simple factory, i les factories injectables tipus Supplier apareixen per tot l'API de streams i col·leccions.

Quan usar-lo i quan no

Usa'l quan:

  • Una classe amb lògica comuna no pot (ni ha de) anticipar la classe concreta dels objectes que necessita, i aquest eix de variació és real (canals de notificació de PideYa: reals i creixents).
  • Vols que tercers o mòduls independents estenguin el teu sistema amb nous productes sense tocar el nucli (frameworks: és el patró dels hot spots d'extensió).
  • Necessites que la construcció del producte quedi al costat de la variant que l'usa, no dispersa.

No l'usis quan:

  • Hi ha un sol producte i cap variació a la vista: un new ben situat o una simple factory basten. Recorda l'escala de la lliçó: new directe → simple factory → Factory Method; puja un esglaó només quan l'actual faci mal.
  • El que varia no és un producte sinó famílies senceres de productes que han de ser coherents entre si: aquest és el territori de la següent lliçó.
  • La complexitat és en el procés de construcció (molts passos i opcions), no en l'elecció de la classe: això demana Builder.

Relació amb altres patrons

Només com a menció, per al teu mapa mental: Abstract Factory s'implementa habitualment com un conjunt de factory methods; Template Method és el patró germà —notificarCanviComanda és de fet un mètode plantilla el pas variable del qual és la creació—; Prototype és l'alternativa que evita la jerarquia de creadors clonant exemplars; i les fàbriques solen exposar-se com a Singleton o, millor, injectades.

Errors Comuns i Consells

  • Anomenar "Factory Method" qualsevol classe amb "Factory" al nom. La simple factory estàtica no és el patró GoF; no és cap pecat usar-la (al contrari!), però en una entrevista o una revisió convé distingir: el patró GoF implica un creador extensible la variació del qual decideix el producte.
  • Crear jerarquies de creadors sense lògica comuna. Si ServeiNotificacions no tingués cap feina pròpia, la jerarquia sencera seria cerimònia: el patró compensa quan el creador aporta lògica que reutilitzen totes les variants. Sense ella, un Supplier injectat dona el mateix amb una classe menys.
  • Deixar el switch viu dins del "factory method". Un crearNotificador() amb un switch dins de cada subclasse indica que la separació per subclasses no es va fer de debò. Cada ConcreteCreator ha de ser avorridament simple.
  • Ignorar la construcció amb dependències. Els productes reals necessiten claus, configuració, col·laboradors. Fixa't on els vam posar: al creador concret (o a la lambda registrada), mai al client ni a la classe base.
  • Consell: anomena els factory methods amb intenció (crearNotificador, no getNotificador): "crear" comunica que cada crida pot fabricar una instància nova, mentre que "get" suggereix retornar una cosa ja existent.

Exercicis

Exercici 1: afegir un canal sense tocar res

Partint de la implementació completa de la secció 4, afegeix el canal WhatsApp (necessita un tokenMeta per construir-se). Escriu les classes noves i assenyala quins fitxers existents has hagut de modificar.

Exercici 2: refactoritzar a registre amb lambdes

El mòdul de PideYa "avisos a repartidors" té aquesta fàbrica tancada:

public class FabricaAvisosRepartidor {
    public static AvisRepartidor crear(TipusAvis tipus) {
        switch (tipus) {
            case NOVA_COMANDA: return new AvisNovaComanda();
            case CANCELLACIO:  return new AvisCancellacio();
            default: throw new IllegalArgumentException();
        }
    }
}

Refactoritza-la a un registre basat en Supplier<AvisRepartidor> que permeti al futur mòdul "propines" registrar el seu AvisPropinaRebuda sense editar aquesta classe.

Exercici 3: identificar els rols

Al JDK, java.util.Calendar.getInstance() retorna un GregorianCalendar o una altra subclasse segons la configuració regional, i a les col·leccions, Iterable.iterator() obliga cada col·lecció a decidir quin Iterator concret retorna. Per a cada cas, identifica quin rol GoF compleix cada peça i raona quin dels dos és un Factory Method canònic i quin s'assembla més a una simple factory.

Solucions

Solució 1:

public class NotificadorWhatsApp implements Notificador {
    private final String tokenMeta;
    public NotificadorWhatsApp(String tokenMeta) { this.tokenMeta = tokenMeta; }
    @Override
    public void enviar(Client client, String missatge) {
        System.out.println("[WHATSAPP a " + client.getTelefon() + "] " + missatge);
    }
}

public class ServeiNotificacionsWhatsApp extends ServeiNotificacions {
    private final String tokenMeta;
    public ServeiNotificacionsWhatsApp(String tokenMeta) { this.tokenMeta = tokenMeta; }
    @Override
    protected Notificador crearNotificador() { return new NotificadorWhatsApp(tokenMeta); }
}

Fitxers existents modificats: cap de la lògica del patró. Només l'arrel de composició (el lloc que decideix quin servei rep cada client) coneixerà la nova opció, que és exactament el punt dissenyat per canviar. Això és OCP funcionant.

Solució 2:

public class RegistreAvisosRepartidor {

    private final Map<TipusAvis, Supplier<AvisRepartidor>> fabriques =
            new EnumMap<>(TipusAvis.class);

    public void registrar(TipusAvis tipus, Supplier<AvisRepartidor> fabrica) {
        fabriques.put(tipus, fabrica);
    }

    public AvisRepartidor crear(TipusAvis tipus) {
        Supplier<AvisRepartidor> f = fabriques.get(tipus);
        if (f == null) throw new IllegalArgumentException("Tipus sense registrar: " + tipus);
        return f.get();
    }
}

// Nucli, a l'arrencada:
registre.registrar(TipusAvis.NOVA_COMANDA, AvisNovaComanda::new);
registre.registrar(TipusAvis.CANCELLACIO,  AvisCancellacio::new);

// Modul "propines", a la SEVA arrencada, sense tocar el nucli:
registre.registrar(TipusAvis.PROPINA, AvisPropinaRebuda::new);

(Nota honesta: afegir PROPINA a l'enum TipusAvis sí que toca un fitxer compartit; si això molestés, la clau del mapa passaria a ser un String o un tipus registrable, amb el cost en seguretat de tipus que això implica.)

Solució 3: a Iterable.iterator(): Creator = Iterable/Collection (amb moltíssima lògica comuna a AbstractCollection que usa l'iterador), ConcreteCreator = ArrayList, HashSet..., Product = Iterator, ConcreteProduct = l'iterador intern de cada col·lecció. És el Factory Method canònic: la subclasse decideix el producte i el codi comú el consumeix. Calendar.getInstance(), en canvi, és un mètode estàtic que decideix internament quina subclasse retornar segons la configuració: malgrat el nom il·lustre, funciona com una simple factory (no hi ha subclasses del creador decidint el producte).

Conclusió

Factory Method t'ha donat el moviment fonamental de la família: extreure la decisió d'instanciació a un punt amb nom —un mètode de fabricació— i fer aquest punt extensible, sigui per herència (la forma GoF, àmbit de classe) o per composició amb Supplier i registres (la forma moderna). També has après a situar la simple factory al seu lloc just: un idiom valuosíssim que no és el patró, i el primer esglaó d'una escala (new → simple factory → Factory Method) que només es puja quan l'esglaó actual fa mal.

Però el nostre factory method fabrica productes d'un en un, i hi ha problemes on això no basta: quan PideYa creui fronteres necessitarà crear conjunts d'objectes que han de ser coherents entre si —passarel·la de pagament, impostos i tiquet del mateix país, sense barreges—. Fabricar famílies completes és la feina de l'Abstract Factory.

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