Adapter va tancar la lliçó anterior amb una promesa: deixar d'arreglar desencaixos a posteriori i aprendre a dissenyar a priori perquè dues dimensions que variaran no se soldin mai. Aquest és exactament el patró Bridge, i PideYa té el cas de llibre esperant-lo: les notificacions. Hi ha tipus de notificació (confirmació de comanda, avís de retard, promoció) i hi ha canals d'enviament (push, SMS, email). Dos eixos independents que, modelats amb herència ingènua, es multipliquen entre si. Bridge els separa en dues jerarquies connectades per un pont, perquè cada eix creixi sense assabentar-se de l'altre.

Contingut

  1. El problema a PideYa: l'explosió tipus × canal
  2. Intenció i estructura del patró
  3. Implementació Java completa
  4. Créixer pels dos eixos
  5. Bridge davant d'Adapter (i una menció a Strategy)
  6. Quan usar-lo i quan no
  7. Errors comuns, exercicis i conclusió

El problema a PideYa: l'explosió tipus × canal

Al mòdul 2 vam resoldre qui crea els notificadors de cada canal (NotificadorPush, NotificadorSms, NotificadorEmail, triats via RegistreNotificadors). Però el producte va créixer: ja no n'hi ha prou amb "enviar un text per un canal". Cada tipus de notificació té la seva pròpia lògica de negoci:

  • Confirmació de comanda: inclou número de comanda, resum de línies i temps estimat; s'envia sempre.
  • Avís de retard: calcula la nova hora estimada, hi adjunta una disculpa i, si el retard supera els 20 minuts, un cupó compensatori.
  • Promoció: text de màrqueting amb condicions legals; només s'envia a clients que han donat consentiment.

El primer intent de l'equip va ser estendre la jerarquia existent per herència, una subclasse per combinació:

// Primer intent: una subclasse per CADA combinacio. NO imitar.
public class ConfirmacioPush extends NotificadorPush { /* munta el text de confirmacio i l'envia per push */ }
public class ConfirmacioSms extends NotificadorSms { /* el MATEIX text, per SMS */ }
public class ConfirmacioEmail extends NotificadorEmail { /* el MATEIX text, per email */ }
public class RetardPush extends NotificadorPush { /* ... */ }
public class RetardSms extends NotificadorSms { /* ... */ }
public class RetardEmail extends NotificadorEmail { /* ... */ }
public class PromocioPush extends NotificadorPush { /* ... */ }
// ... i continuen

Amb 3 tipus i 3 canals, 9 classes. Màrqueting demana notificacions de "valora la teva comanda" (tipus nou): +3 classes. Operacions contracta WhatsApp (canal nou): +4 classes (una per cada tipus existent). La jerarquia creix com el producte dels dos eixos: tipus × canals. I hi ha un dany pitjor que la quantitat: la duplicació. La lògica de "quan toca cupó compensatori" està copiada a RetardPush, RetardSms i RetardEmail; el dia que canviï el llindar de 20 minuts caldrà tocar tres classes i algú n'oblidarà una. DRY, dinamitat.

El diagnòstic precís: la jerarquia única està intentant capturar dues dimensions de variació independents —què es notifica i per on s'envia— amb un sol mecanisme, l'herència, que només sap créixer en un eix. La solució no és més herència: és partir la jerarquia en dues i connectar-les amb composició.

Intenció i estructura del patró

Intenció (GoF): desacoblar una abstracció de la seva implementació, de manera que totes dues puguin variar independentment.

Els noms del GoF despisten, així que traduïm-los abans del diagrama:

  • Abstracció (Abstraction): la jerarquia orientada al negoci/client; aquí, els tipus de notificació (què es diu, quan, amb quines regles).
  • Implementació (Implementor): la jerarquia orientada a la plataforma/tècnica; aquí, els canals d'enviament (com arriba físicament el missatge).
  • El pont: l'abstracció conté una referència a l'implementor i hi delega la part tècnica. Aquesta fletxa de composició entre les dues jerarquies és el patró sencer.
classDiagram
    class Notificacio {
        <<abstract>>
        #canal: CanalEnviament
        +Notificacio(canal: CanalEnviament)
        +notificar(comanda: Comanda)*
    }
    class NotificacioConfirmacio {
        +notificar(comanda: Comanda)
    }
    class NotificacioRetard {
        -minutsRetard: int
        +notificar(comanda: Comanda)
    }
    class NotificacioPromocio {
        -campanya: Campanya
        +notificar(comanda: Comanda)
    }
    class CanalEnviament {
        <<interface>>
        +enviar(destinatari: Client, titol: String, cos: String)
    }
    class CanalPush {
        +enviar(destinatari, titol, cos)
    }
    class CanalSms {
        +enviar(destinatari, titol, cos)
    }
    class CanalEmail {
        +enviar(destinatari, titol, cos)
    }

    Notificacio <|-- NotificacioConfirmacio
    Notificacio <|-- NotificacioRetard
    Notificacio <|-- NotificacioPromocio
    CanalEnviament <|.. CanalPush
    CanalEnviament <|.. CanalSms
    CanalEnviament <|.. CanalEmail
    Notificacio o-- CanalEnviament : pont
Rol GoF A PideYa
Abstraction Notificacio (classe base amb la referència al canal)
RefinedAbstraction NotificacioConfirmacio, NotificacioRetard, NotificacioPromocio
Implementor CanalEnviament
ConcreteImplementor CanalPush, CanalSms, CanalEmail

Compte de la vella: amb Bridge, 3 tipus + 3 canals = 3 + 3 = 6 classes (més una base i una interfície) en lloc de 3 × 3 = 9. Amb 5 tipus i 4 canals: 9 davant de 20. L'herència multiplicava; la composició suma. I cada regla de negoci viu en una sola classe de la jerarquia de tipus, escrita una vegada.

Implementació Java completa

L'implementor i els seus canals concrets — deliberadament ximples: només saben fer arribar un títol i un cos a un client pel seu mitjà:

public interface CanalEnviament {
    void enviar(Client destinatari, String titol, String cos);
}

public class CanalPush implements CanalEnviament {
    @Override
    public void enviar(Client destinatari, String titol, String cos) {
        // Busca els tokens de dispositiu del client i envia via FCM/APNs
        RegistreEsdeveniments.INSTANCE.info("Push a " + destinatari.getId() + ": " + titol);
    }
}

public class CanalSms implements CanalEnviament {
    @Override
    public void enviar(Client destinatari, String titol, String cos) {
        // SMS sense titol: es concatena, i es retalla a 160 caracters
        String text = (titol + ". " + cos);
        smsGateway.enviar(destinatari.getTelefon(),
                          text.length() <= 160 ? text : text.substring(0, 157) + "...");
    }
}

public class CanalEmail implements CanalEnviament {
    @Override
    public void enviar(Client destinatari, String titol, String cos) {
        servidorSmtp.enviar(destinatari.getEmail(), titol, plantillaHtml(cos));
    }
}

Observa que cada canal resol les seves particularitats tècniques (l'SMS no té títol i limita caràcters; l'email maqueta HTML) sense saber mai què està enviant: una confirmació?, una promoció? No és assumpte seu.

L'abstracció i els seus refinaments — on viu el negoci:

public abstract class Notificacio {

    protected final CanalEnviament canal;   // <- el pont

    protected Notificacio(CanalEnviament canal) {
        this.canal = canal;
    }

    /** Cada tipus decideix que comunicar i quan; el COM es delega en el canal. */
    public abstract void notificar(Comanda comanda);
}

public class NotificacioConfirmacio extends Notificacio {

    public NotificacioConfirmacio(CanalEnviament canal) { super(canal); }

    @Override
    public void notificar(Comanda comanda) {
        String titol = "Comanda " + comanda.getNumero() + " confirmada";
        String cos = "La teva comanda de " + comanda.getRestaurant().getNom()
                + " arribara cap a les " + comanda.getHoraEstimada() + ".";
        canal.enviar(comanda.getClient(), titol, cos);
    }
}

public class NotificacioRetard extends Notificacio {

    private static final int MINUTS_PER_CUPO = 20;   // la regla, UNA sola vegada
    private final int minutsRetard;

    public NotificacioRetard(CanalEnviament canal, int minutsRetard) {
        super(canal);
        this.minutsRetard = minutsRetard;
    }

    @Override
    public void notificar(Comanda comanda) {
        String titol = "La teva comanda es retarda " + minutsRetard + " minuts";
        String cos = "Ho sentim. Nova hora estimada: "
                + comanda.getHoraEstimada().plusMinutes(minutsRetard) + ".";
        if (minutsRetard > MINUTS_PER_CUPO) {
            cos += " Et regalem un cupo del 10% per a la teva propera comanda.";
            serveiCupons.emetreCompensacio(comanda.getClient());
        }
        canal.enviar(comanda.getClient(), titol, cos);
    }
}

public class NotificacioPromocio extends Notificacio {

    private final Campanya campanya;

    public NotificacioPromocio(CanalEnviament canal, Campanya campanya) {
        super(canal);
        this.campanya = campanya;
    }

    @Override
    public void notificar(Comanda comanda) {
        Client client = comanda.getClient();
        if (!client.haDonatConsentimentMarketing()) {
            return;                                    // la regla legal, UNA sola vegada
        }
        canal.enviar(client, campanya.getTitol(),
                     campanya.getText() + "\n" + campanya.getCondicionsLegals());
    }
}

El client creua el pont en construir: qualsevol tipus amb qualsevol canal, decidit en execució (per preferències de l'usuari, per criticitat del missatge, per cost del canal):

CanalEnviament canalPreferit = client.prefereixSms() ? new CanalSms() : new CanalPush();

new NotificacioConfirmacio(canalPreferit).notificar(comanda);
new NotificacioRetard(new CanalSms(), 25).notificar(comanda);      // retards: sempre SMS
new NotificacioPromocio(new CanalEmail(), campanya).notificar(comanda); // promos: email, mes barat

Les 9 combinacions existeixen i funcionen, però cap no té classe pròpia: són composicions muntades al vol. I la creació dels canals no ha de ser per força un new a la vista: el RegistreNotificadors del mòdul 2 pot reconvertir-se en registre de Supplier<CanalEnviament> i continuar sent el punt únic on es decideix el canal — els patrons creacionals fabriquen les puntes del pont; Bridge en decideix la forma.

Créixer pels dos eixos

La prova de foc de la separació és el cost de créixer:

  • Tipus nou ("valora la teva comanda"): una classe, NotificacioValoracio extends Notificacio. Els canals ni es recompilen.
  • Canal nou (WhatsApp): una classe, CanalWhatsApp implements CanalEnviament. Els tipus ni se n'assabenten, i automàticament tots els tipus existents saben sortir per WhatsApp.

Cada eix compleix OCP pel seu compte: extensió sense modificació, en els dos sentits. Compara-ho amb la jerarquia multiplicativa, on el canal nou costava una classe per cada tipus i escampava la lògica duplicada una mica més.

Una decisió de disseny que veuràs en implementacions reals: la interfície de l'implementor no ha d'imitar la de l'abstracció. CanalEnviament.enviar(destinatari, titol, cos) és més primitiva que Notificacio.notificar(comanda): l'abstracció treballa en termes de negoci (comandes, campanyes, consentiments) i tradueix als termes tècnics del canal (títol, cos, destinatari). Aquesta asimetria és sana; si les dues interfícies fossin idèntiques, sospitaries amb raó que una de les dues capes no aporta res.

Bridge davant d'Adapter (i una menció a Strategy)

Bridge i Adapter es confonen perquè el dibuix local és semblant: un objecte que delega en un altre darrere d'una interfície. La diferència és en el quan i el perquè, i és taxativa:

Aspecte Adapter Bridge
Moment A posteriori: les peces ja existeixen i no encaixen A priori: es dissenya abans que les jerarquies creixin
Problema Interfícies incompatibles que no controles Una jerarquia que amenaça de créixer per producte de dos eixos
Les interfícies implicades Aliena (adaptee) i pròpia (target): es tradueixen Totes dues pròpies: es dissenyen per col·laborar
Sabor Arranjament, duana, pedaç honest Arquitectura, planificació de la variació
A PideYa AdaptadorPayPal per a un SDK heretat Notificacio × CanalEnviament dissenyat per nosaltres

Una frase per endur-se: Adapter fa que coses que ja existeixen encaixin; Bridge evita que coses que existiran se soldin. De fet conviuen bé: si demà WhatsApp només ofereix un SDK amb interfície alienígena, CanalWhatsApp serà internament un Adapter d'aquell SDK... penjat del pont com un implementor més.

Strategy (només menció, es desenvolupa a la lliçó 04-10): estructuralment, el pont cap a CanalEnviament és gairebé idèntic a una estratègia intercanviable. La diferència tornarà a ser d'intenció —Strategy intercanvia algorismes dins d'un objecte; Bridge separa jerarquies senceres que creixen pel seu compte— i l'afinarem allà. De moment, queda't que si només hi hagués un tipus de notificació i tres maneres d'enviar-la, allò seria més Strategy que Bridge; el pont es justifica quan els dos costats tenen jerarquia.

Quan usar-lo i quan no

Usa'l quan:

  • Detectes (o preveus amb evidència) dues dimensions de variació independents en una mateixa responsabilitat: què/com, negoci/plataforma, model/render, operació/persistència.
  • Les subclasses comencen a dir-se CosaAmbCosa (ConfirmacioPush, InformePdfMensual): el nom compost delata els dos eixos fosos.
  • Vols poder canviar la implementació en execució (canal segons preferències del client) o repartir el desenvolupament de cada jerarquia a equips diferents.

No l'usis quan:

  • Només varia un eix: una jerarquia normal (o una Strategy) basta, i el pont seria una indirecció gratuïta — la sobreenginyeria de la lliçó de la balança.
  • La "segona dimensió" és especulativa: "potser algun dia hi haurà més canals" sense cap encàrrec real és YAGNI de manual. El símptoma honest és l'explosió ja iniciada (aquelles 9 classes) o l'encàrrec signat del quart canal.
  • Les dues jerarquies no són independents de debò: si cada tipus de notificació necessita conèixer detalls íntims de cada canal, el pont es converteix en un colador d'instanceof i és millor replantejar el repartiment de responsabilitats.

Relació amb altres patrons (només menció): Abstract Factory pot crear parells abstracció-implementor coherents; els implementors concrets poden ser Adapters d'SDKs externs; Decorator pot embolcallar un CanalEnviament per afegir-hi reintents o logging sense tocar el pont; i la comparació fina amb Strategy i State arribarà al mòdul 4.

Errors Comuns i Consells

  • Abstracció que ponteja... i després pregunta. Si NotificacioRetard fa if (canal instanceof CanalSms) per escurçar el text, el pont està foradat: aquella decisió (limitar caràcters) pertany al canal. Tot coneixement tècnic, al costat tècnic.
  • Implementor gras. Una interfície CanalEnviament amb 15 mètodes (adjunts, HTML, justificants de recepció, prioritats...) obliga cada canal a implementar coses que no suporta. Mantén l'implementor primitiu i deixa la riquesa a l'abstracció; si un subconjunt de canals comparteix capacitats extres, segrega interfícies (ISP, lliçó 01-03).
  • Confondre els costats. Posar les regles de consentiment a CanalEmail "perquè les promos van per email". El dia que les promos surtin també per push, la regla legal és al lloc equivocat. Negoci a l'abstracció, tècnica a l'implementor, sempre.
  • Bridge pòstum. Introduir el pont quan ja hi ha 20 classes combinatòries és més car que fer-ho amb 6, però continua sent rendible; fes-ho per passos (apareix CanalEnviament, cada TipusCanal delega, es fonen els duplicats) i amb tests pel mig. El que no és rendible és introduir-lo quan no hi ha ni hi haurà segon eix.
  • Consell: el test de foc per ubicar un codi dubtós és preguntar "això canvia si canvia el canal, o si canvia el negoci?". El llindar del cupó no canvia per usar WhatsApp: abstracció. El límit de 160 caràcters no canvia per ser una promo: implementor.

Exercicis

Exercici 1: notificacions també per a repartidors

PideYa vol notificar també els repartidors (nova assignació de comanda, canvi de zona), i els repartidors només reben push o SMS. Indica quines classes noves necessites i quines d'existents es modifiquen. Pista: el destinatari és un eix nou o cap en els existents?

Exercici 2: detectar el pont trencat

Què hi ha malament aquí i com ho arreglaries?

public class CanalEmail implements CanalEnviament {
    @Override
    public void enviar(Client destinatari, String titol, String cos) {
        if (titol.startsWith("Comanda") && titol.contains("confirmada")) {
            cos += "\nGracies per confiar en PideYa.";  // signatura nomes en confirmacions
        }
        servidorSmtp.enviar(destinatari.getEmail(), titol, plantillaHtml(cos));
    }
}

Exercici 3: comptar classes

L'equip d'informes té InformeVendesPdf, InformeVendesExcel, InformeRepartimentsPdf, InformeRepartimentsExcel, InformeClientsPdf i InformeClientsExcel, i arriben encàrrecs d'un format HTML i un informe de promocions. (a) Quantes classes hi haurà sense Bridge quan es completin tots dos encàrrecs? (b) I amb Bridge (compta jerarquies, base i interfície)? (c) Anomena els rols GoF resultants.

Solucions

Solució 1: no cal cap eix nou; cal generalitzar el destinatari. Canvis: (1) CanalEnviament.enviar passa a rebre un tipus comú (Destinatari, interfície que implementen Client i Repartidor, amb el que cada canal necessita: telèfon, tokens, email) — única modificació a existents, mecànica. (2) Classes noves, totes refinaments de l'abstracció: NotificacioAssignacio extends Notificacio i NotificacioCanviZona extends Notificacio. Els canals concrets no es toquen (push i SMS ja existeixen); que els repartidors no usin email és una regla de qui compon, no una classe nova: simplement mai no es construeix una notificació de repartidor amb CanalEmail (i pot blindar-se a la fàbrica que compon). Total: 2 classes noves + 1 interfície de destinatari. Amb la jerarquia multiplicativa haurien estat 4 classes noves i sumant.

Solució 2: el canal està ensumant el contingut per inferir el tipus de negoci (parsejar el títol per saber si és una confirmació): coneixement del costat del negoci infiltrat al costat tècnic, i a més fràgil (es trenca en traduir el títol o canviar el text). Arranjament: la signatura "Gracies per confiar en PideYa" és una decisió del tipus de notificació → s'afegeix al cos a NotificacioConfirmacio.notificar(...), i CanalEmail torna a ser ximple. Si el que es vol és una signatura a tots els emails (decisió sí tècnica del canal), llavors s'afegeix incondicionalment al canal, sense mirar el títol.

Solució 3: (a) Sense Bridge: eixos 4 informes × 3 formats = 12 classes combinatòries. (b) Amb Bridge: 4 refinaments (InformeVendes, InformeRepartiments, InformeClients, InformePromocions) + 3 implementors (FormatPdf, FormatExcel, FormatHtml) + base Informe + interfície FormatSortida = 9 classes/interfícies, i sobretot creixement futur additiu, no multiplicatiu. (c) Rols: Abstraction Informe; RefinedAbstraction els quatre informes; Implementor FormatSortida; ConcreteImplementor els tres formats.

Conclusió

Bridge parteix en dues una jerarquia que creixia per producte: a un costat l'abstracció (els tipus de notificació, amb tot el negoci), a l'altre la implementació (els canals, amb tota la tècnica), i entre tots dos una referència de composició que es creua en execució. El resultat és creixement additiu en els dos eixos, regles escrites una sola vegada i equips que poden fer evolucionar cada costat sense trepitjar-se. I va quedar traçada la frontera amb Adapter —disseny a priori davant d'arranjament a posteriori— que tornarà a la comparativa final.

Fins ara hem connectat peces de dues en dues: adaptador i adaptee, abstracció i implementor. Però hi ha estructures a PideYa que no són parelles sinó arbres: la carta d'un restaurant té seccions, a dins subseccions, a dins plats... i volem preguntar el preu o la disponibilitat a qualsevol node sense preguntar-nos primer què és. Tractar el grup igual que l'individu: aquesta uniformitat recursiva és el pròxim patró. Ens veiem a Composite.

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