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
- El problema a PideYa: l'explosió tipus × canal
- Intenció i estructura del patró
- Implementació Java completa
- Créixer pels dos eixos
- Bridge davant d'Adapter (i una menció a Strategy)
- Quan usar-lo i quan no
- 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 continuenAmb 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 baratLes 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'
instanceofi é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
NotificacioRetardfaif (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
CanalEnviamentamb 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, cadaTipusCanaldelega, 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
- 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
