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

  1. El deute de la lliçó 01-01, rellegit amb ulls de mòdul 4
  2. Intenció i estructura del patró
  3. Implementació Java completa
  4. Push vs. pull: quant explicar a la notificació
  5. Detalls que mosseguen: ordre, errors i fuites de listeners
  6. Observer al JDK i a les UI
  7. D'Observer a esdeveniments i pub-sub (la porta al mòdul 6)
  8. Quan usar-lo i quan no
  9. Relació amb altres patrons
  10. Errors comuns
  11. 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 new cablejats: el problema creacional que el mòdul 2 va desmuntar — impossible testejar sense enviar SMS reals.
  • Comanda coneix 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) sense ConcurrentModificationException. 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/catch per 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 (ActionListener de Swing, els listeners d'Android/JavaFX, els addEventListener del DOM): cada botó és un subject; cada onClick, un observador. Tota la programació d'interfícies és Observer industrialitzat.
  • PropertyChangeListener/PropertyChangeSupport (java.beans): un subject reutilitzable per composició — la teva classe delega en PropertyChangeSupport la 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 nostre ObservadorComanda) 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 canviarEstat ningú no sap què passarà, cal rastrejar les subscripcions. En sistemes saturats d'observadors, depurar és arqueologia. Bon logging de subscripció/notificació (via RegistreEsdeveniments) é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: comandaActualitzada que crida comanda.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 CopyOnWriteArrayList t'ho regala).
  • Empassar-se les excepcions sense registrar-les: aïllar no és silenciar; el catch sense 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

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