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
- El problema a PideYa
- Primer pas: la simple factory (que no és un patró GoF)
- El patró Factory Method: estructura GoF
- Implementació Java completa
- Variants modernes: genèrics i lambdes
- Quan usar-lo i quan no
- Relació amb altres patrons
- 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 canalQuè 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
newben situat o una simple factory basten. Recorda l'escala de la lliçó:newdirecte → 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
ServeiNotificacionsno 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, unSupplierinjectat dona el mateix amb una classe menys. - Deixar el
switchviu dins del "factory method". UncrearNotificador()amb unswitchdins 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, nogetNotificador): "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
- Què són els Patrons de Disseny?
- Història i Origen dels Patrons de Disseny
- Principis de Disseny: SOLID i Altres Fonaments
- UML Essencial per Entendre Patrons
- Classificació dels Patrons de Disseny
- Avantatges i Desavantatges d'Usar Patrons de Disseny
Mòdul 2: Patrons Creacionals
- Introducció als Patrons Creacionals
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa i Elecció de Patrons Creacionals
Mòdul 3: Patrons Estructurals
- Introducció als Patrons Estructurals
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa i Elecció de Patrons Estructurals
Mòdul 4: Patrons de Comportament
- Introducció als Patrons de Comportament
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa i Elecció de Patrons de Comportament
Mòdul 5: Aplicació de Patrons de Disseny
- Com Seleccionar el Patró Adequat
- Exemples Pràctics d'Ús de Patrons
- Patrons de Disseny en Projectes Reals
- Refactorització Usant Patrons de Disseny
- Antipatrons: Quan els Patrons es Tornen un Problema
Mòdul 6: Patrons de Disseny Avançats
- Patrons de Disseny en Arquitectures Modernes
- Patrons de Disseny en Microserveis
- Patrons de Disseny en Sistemes Distribuïts
- Patrons de Concurrència
- Patrons de Disseny en Desenvolupament Àgil
