Aquesta lliçó salda el deute més antic del curs. A la primera lliçó vas veure un Comanda.canviarEstat(...) que, a més de canviar l'estat, creava amb new un ServeiPush, un ServeiSms i un PanellEstadistiques i els cridava un a un. Vam dir llavors: "existeix una estructura clàssica que resol exactament això... i té nom". Tres mòduls després, aquí la tens. Observer inverteix la relació: la comanda deixa de conèixer els seus interessats i són els interessats els qui se subscriuen a la comanda. Un canvia, molts se n'assabenten, ningú no coneix ningú. És probablement el patró de comportament més influent del catàleg — d'ell descendeixen els listeners de tota UI, els esdeveniments dels frameworks i, en última instància, les arquitectures d'esdeveniments que veurem al mòdul 6.
Contingut
- El deute de la lliçó 01-01, rellegit amb ulls de mòdul 4
- Intenció i estructura del patró
- Implementació Java completa
- Push vs. pull: quant explicar a la notificació
- Detalls que mosseguen: ordre, errors i fuites de listeners
- Observer al JDK i a les UI
- D'Observer a esdeveniments i pub-sub (la porta al mòdul 6)
- Quan usar-lo i quan no
- Relació amb altres patrons
- Errors comuns
- Exercicis i conclusió
El deute de la lliçó 01-01, rellegit amb ulls de mòdul 4
El codi del crim, tal com el vam deixar a la primera lliçó:
public void canviarEstat(String nouEstat) {
this.estat = nouEstat;
ServeiPush push = new ServeiPush();
push.enviar(this.getClient().getTokenDispositiu(), "La teva comanda esta: " + nouEstat);
ServeiSms sms = new ServeiSms();
sms.enviar(this.getRepartidor().getTelefon(), "Comanda " + this.getId() + ": " + nouEstat);
PanellEstadistiques panell = new PanellEstadistiques();
panell.registrarCanviEstat(this.getId(), nouEstat);
}Llavors només feia mala olor; ara pots anomenar cada delicte amb el que has après:
- Els
newcablejats: el problema creacional que el mòdul 2 va desmuntar — impossible testejar sense enviar SMS reals. Comandaconeix els detalls de push, SMS i estadístiques: violació de DIP i SRP alhora.- Cada interessat nou (email, fidelització, cuina) modifica
Comanda: violació frontal d'OCP. - El nombre d'interessats és variable i creixent — la definició mateixa del context que demana aquest patró: un objecte de negoci els canvis del qual interessen a un nombre variable de tercers que ell no hauria de conèixer.
La inversió que ho arregla: Comanda no crida els seus interessats; manté una llista de subscriptors anònims i, quan canvia, la recorre avisant a través d'una interfície mínima. Qui és a la llista es decideix fora, en configuració — i pot canviar en execució.
Intenció i estructura del patró
Intenció (GoF): definir una dependència un-a-molts entre objectes, de manera que quan un objecte canvia d'estat, tots els seus dependents són notificats i actualitzats automàticament.
classDiagram
class ObservadorComanda {
<<interface>>
+comandaActualitzada(comanda: Comanda, anterior: EstatComanda)
}
class Comanda {
-observadors: List~ObservadorComanda~
-estat: EstatComanda
+subscriure(o: ObservadorComanda)
+dessubscriure(o: ObservadorComanda)
+canviarEstat(nou: EstatComanda)
-notificarObservadors(anterior: EstatComanda)
}
class NotificadorClient {
-notificador: Notificador
+comandaActualitzada(comanda, anterior)
}
class NotificadorRepartidor {
+comandaActualitzada(comanda, anterior)
}
class MonitorCuina {
+comandaActualitzada(comanda, anterior)
}
class PanellEstadistiques {
+comandaActualitzada(comanda, anterior)
}
ObservadorComanda <|.. NotificadorClient
ObservadorComanda <|.. NotificadorRepartidor
ObservadorComanda <|.. MonitorCuina
ObservadorComanda <|.. PanellEstadistiques
Comanda o-- ObservadorComanda : notifica
| Rol GoF | A PideYa |
|---|---|
| Subject (manté subscriptors i notifica) | Comanda |
| Observer (interfície mínima d'avís) | ObservadorComanda |
| ConcreteObserver | NotificadorClient, NotificadorRepartidor, MonitorCuina, PanellEstadistiques |
L'essencial és en la direcció de les fletxes: Comanda depèn només de la interfície ObservadorComanda — mai d'un observador concret. Els concrets depenen de Comanda (en llegeixen les dades), però Comanda no sap ni quants són ni què fan. La dependència un-a-molts existeix en execució (la llista), no en compilació.
sequenceDiagram
participant F as FacanaCheckout
participant P as Comanda (Subject)
participant NC as NotificadorClient
participant NR as NotificadorRepartidor
participant PE as PanellEstadistiques
F->>P: canviarEstat(EN_REPARTIMENT)
P->>P: estat = EN_REPARTIMENT
P->>NC: comandaActualitzada(this, PAGADA)
NC->>NC: push al client
P->>NR: comandaActualitzada(this, PAGADA)
NR->>NR: SMS al repartidor
P->>PE: comandaActualitzada(this, PAGADA)
PE->>PE: registrar metrica
Note over P: Comanda no sap que ha fet cadascu
Implementació Java completa
La interfície Observer. Un mètode, ben anomenat per l'esdeveniment. Passem la comanda i l'estat anterior — l'elecció push/pull la discutim a la secció següent:
public interface ObservadorComanda {
void comandaActualitzada(Comanda comanda, EstatComanda estatAnterior);
}El Subject. El canviarEstat redimit, tres mòduls després:
public class Comanda {
private final List<ObservadorComanda> observadors = new CopyOnWriteArrayList<>();
private EstatComanda estat = EstatComanda.CREADA;
public void subscriure(ObservadorComanda observador) {
observadors.add(Objects.requireNonNull(observador));
}
public void dessubscriure(ObservadorComanda observador) {
observadors.remove(observador);
}
public void canviarEstat(EstatComanda nou) {
if (nou == this.estat) {
return; // sense canvi real, sense soroll
}
EstatComanda anterior = this.estat;
this.estat = nou; // 1r consolidar l'estat...
notificarObservadors(anterior); // ...2n avisar
}
private void notificarObservadors(EstatComanda anterior) {
for (ObservadorComanda o : observadors) {
try {
o.comandaActualitzada(this, anterior);
} catch (RuntimeException e) {
// Un observador trencat no pot tombar la notificacio dels altres
RegistreEsdeveniments.INSTANCIA.error("Observador ha fallat: " + o.getClass(), e);
}
}
}
}Decisions comentades, totes amb cicatriu al darrere:
CopyOnWriteArrayList: permet que un observador es dessubscrigui durant una notificació (o que un altre fil se subscrigui) senseConcurrentModificationException. Per a llistes de subscriptors —moltes lectures, poques escriptures— és l'estructura idònia.- Estat primer, avís després: els observadors llegiran
comanda.getEstat(); si notifiques abans de consolidar, llegeixen el passat. try/catchper observador: sense ell, el primer observador que llanci excepció deixa els altres sense el seu avís — i la fallada del panell d'estadístiques no ha d'impedir l'SMS al repartidor.- Filtre de no-canvi: notificar "canvis" que no canvien res genera cascades de soroll (i, en grafs d'observadors, bucles).
Els observadors concrets. Cadascun, una classe petita amb una sola raó de canvi — i reutilitzant peces del curs: el Notificador del Factory Method, potser embolcallat en el NotificadorAmbReintents del Decorator:
public class NotificadorClient implements ObservadorComanda {
private final Notificador notificador; // injectat: push, sms, email... tant se val
public NotificadorClient(Notificador notificador) {
this.notificador = notificador;
}
@Override
public void comandaActualitzada(Comanda comanda, EstatComanda anterior) {
notificador.enviar("La teva comanda " + comanda.getId() + " esta: " + comanda.getEstat());
}
}
public class PanellEstadistiques implements ObservadorComanda {
@Override
public void comandaActualitzada(Comanda comanda, EstatComanda anterior) {
metriques.registrarTransicio(anterior, comanda.getEstat());
}
}El muntatge, en configuració (la FacanaCheckout en crear la comanda, o l'arrencada de l'app):
comanda.subscriure(new NotificadorClient(registreNotificadors.crear("push")));
comanda.subscriure(new NotificadorRepartidor(registreNotificadors.crear("sms")));
comanda.subscriure(new MonitorCuina(pantallaCuina));
comanda.subscriure(new PanellEstadistiques(metriques));Passa revista als delictes de 01-01: els new de serveis, fora de Comanda (i testejar és subscriure un observador fals); els detalls de push/SMS, als seus observadors; l'interessat nou —fidelització que suma punts en arribar a LLIURADA— és una classe nova i una línia de subscripció: Comanda no es toca. Deute saldat.
Push vs. pull: quant explicar a la notificació
La firma del mètode d'avís admet un espectre:
| Model | Firma típica | Avantatge | Inconvenient |
|---|---|---|---|
| Push pur | comandaActualitzada(id, nouEstat, horaEstimada, zona...) |
L'observador no toca el subject; pot ni conèixer-lo | El subject endevina què necessita cadascú; firmes que creixen |
| Pull pur | comandaActualitzada() |
Firma mínima i estable | Tots tornen al subject a preguntar; l'estat pot haver tornat a canviar entre avís i consulta |
| Híbrid | comandaActualitzada(comanda, estatAnterior) |
Referència per consultar + la dada clau de l'esdeveniment | — |
Triem l'híbrid amb motiu: l'estatAnterior només existeix en el moment de l'avís (després, ningú no el recorda — el panell d'estadístiques el necessita per registrar transicions), i la referència a la comanda permet a cada observador llegir el que li interessi sense engreixar la firma. Regla pràctica: empeny allò efímer de l'esdeveniment, deixa en pull allò consultable.
Detalls que mosseguen: ordre, errors i fuites de listeners
Ordre de notificació. El nostre subject notifica en ordre de subscripció, però això és un detall d'implementació: el contracte d'Observer és "tots se n'assabenten", no "en aquest ordre". L'error greu és dissenyar observadors que depenen de l'ordre (l'SMS assumeix que estadístiques ja ha registrat): això són dependències ocultes entre suposats desconeguts, invisibles al codi de muntatge. Si l'ordre importa de debò, Observer és el patró equivocat — vols una orquestració explícita (Facade cridant passos en ordre, o un Mediator dirigint).
Errors. Ja vist al codi: aïllar cada observador amb try/catch. La pregunta de disseny és què més fer — registrar i seguir (la nostra opció), reintentar (el decorador de reintents del mòdul 3 ho fa per nosaltres als notificadors), o dessubscriure el reincident.
Fuites de listeners. El bug de producció més famós del patró: el subject guarda una referència forta a cada observador, així que un observador que oblida dessubscriure's no pot ser recollit mentre visqui el subject — i segueix rebent (i processant) avisos d'una pantalla que l'usuari va tancar fa una hora. A PideYa: la pantalla de seguiment se subscriu a la comanda; l'usuari navega enrere; la pantalla "morta" segueix penjada de la comanda fins que aquesta es lliura. Antídots: dessubscriure simètricament al cicle de vida (en tancar la pantalla), i per a subscriptors de vida curta sobre subjects de vida llarga, referències febles (WeakReference) que permetin al recollidor endur-se'ls.
Observer al JDK i a les UI
El patró amara la plataforma:
- Els listeners d'UI (
ActionListenerde Swing, els listeners d'Android/JavaFX, elsaddEventListenerdel DOM): cada botó és un subject; cadaonClick, un observador. Tota la programació d'interfícies és Observer industrialitzat. PropertyChangeListener/PropertyChangeSupport(java.beans): un subject reutilitzable per composició — la teva classe delega enPropertyChangeSupportla llista i la notificació, en comptes de reimplementar-les.java.util.Observable/Observer: el suport original del JDK, deprecat des de Java 9 — classe en comptes d'interfície (et robava l'herència), sense tipus genèrics, sense ordre garantit. La seva lliçó pòstuma: millor una interfície pròpia de domini (el nostreObservadorComanda) que un observador genèric de talla única.java.util.concurrent.Flow(Java 9+): la versió moderna amb contrapressió (el subscriptor demana quants elements pot processar), base de la programació reactiva (RxJava, Reactor).
D'Observer a esdeveniments i pub-sub (la porta al mòdul 6)
Una generalització natural, només apuntada: si en comptes de subscriure's a una comanda concreta els interessats se subscriuen a tipus d'esdeveniment (ComandaLliurada, ComandaCancelada) en un intermediari —un bus d'esdeveniments—, subject i observadors deixen de conèixer-se fins i tot com a interfícies: publiques un esdeveniment i qui estigui subscrit al tipus el rep. Això és publicar-subscriure (pub-sub); a escala de processos i amb un broker (Kafka, RabbitMQ) pel mig, és la base de les arquitectures orientades a esdeveniments amb què PideYa integrarà cuina, repartiment i facturació al mòdul 6. Tot aquest món és aquest patró amb la llista de subscriptors externalitzada. Aquí ho deixem: menció feta, desenvolupament allà.
Quan usar-lo i quan no
Usa'l quan:
- Un canvi en un objecte interessa a altres el nombre i la identitat dels quals varien (en configuració o en execució).
- L'objecte observat no ha de conèixer els seus interessats (mòduls independents, capes que no s'han de mirar).
- Vols poder afegir reaccions noves sense tocar el qui canvia (OCP).
Evita'l quan:
- Hi ha un sol interessat, fix i per sempre: una crida directa és més clara i depurable.
- Les reaccions han d'ocórrer en ordre garantit o en transacció amb el canvi (tot o res): la difusió desordenada i sense acusament de recepció no dona aquestes garanties; orquestra explícitament.
- La cadena causa-efecte ja és difícil de seguir: el gran cost d'Observer és que el flux es torna invisible — mirant
canviarEstatningú no sap què passarà, cal rastrejar les subscripcions. En sistemes saturats d'observadors, depurar és arqueologia. Bon logging de subscripció/notificació (viaRegistreEsdeveniments) és oxigen.
Relació amb altres patrons
- Mediator: l'acarament estrella del mòdul (cara a cara a la comparativa): Observer difon sense protocol central; Mediator dirigeix amb protocol. I es combinen: un mediador pot assabentar-se de les coses observant els seus col·legues.
- State (lliçó següent): socis directes — State decidirà quines transicions són legals a la comanda; Observer difon les que ocorren.
- Singleton: els subjects globals (un bus d'esdeveniments com a singleton) hereten tots els mals de l'estat global del mòdul 2.
- Decorator: els observadors notificadors reutilitzen els decoradors tècnics (reintents, logging) sense que el subject ho sàpiga.
- Command: els avisos es poden materialitzar com a ordres encuades — primer pas cap a la notificació asíncrona.
Errors comuns
- Oblidar dessubscriure: la fuita de listeners ja descrita; simetria subscriure/dessubscriure al cicle de vida, sempre.
- Feina pesada al fil de l'avís: el nostre bucle és síncron — un observador que triga 3 segons congela
canviarEstat(i en UI, la interfície sencera). El que és lent, a una cua o executor (l'avís encuat com a ordre). - Observadors que muten el subject durant l'avís:
comandaActualitzadaque cridacomanda.canviarEstat(...)provoca notificacions reentrants i bucles. Si una reacció ha de causar una altra transició, que ho demani després (encuant), no dins de l'avís. - Dependre de l'ordre de notificació entre observadors: dependències ocultes; ja explicat.
- Notificar amb el lock posat (en subjects sincronitzats): recepta d'interbloqueig si un observador torna a entrar al subject. Copia la llista i notifica fora de la secció crítica (la
CopyOnWriteArrayListt'ho regala). - Empassar-se les excepcions sense registrar-les: aïllar no és silenciar; el
catchsense log converteix fallades reals en misteris.
Exercicis
Exercici 1: l'observador de fidelització
Implementa ProgramaFidelitzacio implements ObservadorComanda: quan una comanda arriba a LLIURADA, suma al client 1 punt per cada euro del total (serveiPunts.sumar(clientId, punts)). Els altres estats no li interessen. Què has hagut de tocar a Comanda?
Exercici 2: caçar la fuita
La pantalla PantallaSeguiment se subscriu a la comanda al seu constructor. Els usuaris es queixen que l'app va cada cop més lenta durant comandes llargues, i el perfilador mostra centenars d'instàncies de PantallaSeguiment vives. Explica la causa exacta (qui reté qui?) i escriu la correcció.
Exercici 3: Observer o crida directa?
Decideix i justifica: (a) en confirmar la comanda, cobrar a la passarel·la de pagament; (b) en canviar l'estat, refrescar la pantalla de cuina; (c) en lliurar, generar la factura, que legalment ha d'existir abans de donar la comanda per tancada; (d) en canviar el preu d'un plat, invalidar el ProxyCacheCataleg del mòdul 3.
Solucions
Solució 1:
public class ProgramaFidelitzacio implements ObservadorComanda {
private final ServeiPunts serveiPunts;
public ProgramaFidelitzacio(ServeiPunts serveiPunts) {
this.serveiPunts = serveiPunts;
}
@Override
public void comandaActualitzada(Comanda comanda, EstatComanda anterior) {
if (comanda.getEstat() != EstatComanda.LLIURADA) {
return; // filtra: nomes li interessa lliurar
}
int punts = comanda.getTotal().intValue(); // 1 punt per euro
serveiPunts.sumar(comanda.getClient().getId(), punts);
}
}A Comanda: res. Una classe nova i una línia de subscripció al muntatge. Compara-ho amb la versió de 01-01, on això hauria estat el quart bloc cablejat dins de canviarEstat — aquest exercici és la demostració del deute saldat.
Solució 2: la comanda (subject, viva durant hores) guarda referència forta a cada PantallaSeguiment a la seva llista d'observadors; l'usuari obre i tanca la pantalla vint vegades, i cap instància no pot ser recollida perquè la comanda les reté — i les vint segueixen processant cada avís. Correcció: dessubscripció simètrica al cicle de vida de la pantalla:
public class PantallaSeguiment implements ObservadorComanda {
private final Comanda comanda;
public PantallaSeguiment(Comanda comanda) {
this.comanda = comanda;
comanda.subscriure(this); // en obrir
}
public void enTancar() { // hook de cicle de vida de la UI
comanda.dessubscriure(this); // simetria obligatoria
}
// ...
}(Defensa addicional si el cicle de vida no és fiable: que el subject guardi WeakReference<ObservadorComanda> i purgui les mortes en notificar.)
Solució 3: (a) crida directa (orquestrada per la FacanaCheckout): el cobrament és un pas essencial, síncron i amb maneig d'errors propi del flux — no una "reacció opcional"; (b) Observer: la cuina és un interessat més entre diversos, sense ordre ni transacció — MonitorCuina de manual; (c) crida directa/orquestració: hi ha una garantia d'ordre i atomicitat ("abans de tancar") que Observer no ofereix per contracte; (d) Observer: la memòria cau és un interessat tècnic que ni ha de conèixer el catàleg de negoci ni ser conegut — i demà s'hi poden sumar el cercador i l'històric de preus (els interessats variables de la definició).
Conclusió
El deute de la primera lliçó està saldat, i amb interessos: Comanda.canviarEstat(...) ja no crea ni coneix ningú — manté una llista d'ObservadorComanda anònims, consolida el seu estat, i difon l'avís aïllant fallades; client, repartidor, cuina, estadístiques i el nouvingut programa de fidelització reaccionen cadascun a la seva classe, endollats i desendollats en configuració. T'endús a més l'ofici que separa el diagrama del patró en producció: híbrid push/pull amb l'estat anterior, no dependre de l'ordre, aïllar excepcions, i la caça de la fuita de listeners. I una porta entreoberta: externalitza la llista de subscriptors a un bus i tens pub-sub — el mòdul 6 espera.
Però fixa't en el que Observer no vigila: avui res no impedeix canviarEstat(LLIURADA) sobre una comanda acabada de crear i sense pagar — avisaríem tothom, això sí, d'una barbaritat. Quines transicions són legals, i com canvia el comportament de la comanda segons on és al seu cicle de vida —què es pot cancel·lar, què es pot modificar—, és un problema diferent: el comportament que canvia amb l'estat intern. Ens veiem a State.
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
