A la introducció va quedar plantejat el símptoma: PideYa necessita cobrar amb un SDK de pagaments heretat la interfície del qual no s'assembla gens a la nostra PassarellaPagament, i no podem tocar ni l'SDK (no és nostre) ni els clients de PassarellaPagament (són mig sistema). Adapter és el patró de les interfícies incompatibles: l'endoll que permet connectar una peça existent a un forat que no va ser pensat per a ella. És probablement el patró estructural que més vegades escriuràs en la teva carrera, perquè el món real és ple de codi que no encaixa: APIs de tercers, sistemes llegats, llibreries que no trien els teus noms.
Contingut
- El problema a PideYa: l'SDK que no encaixa
- Intenció i estructura: Adapter d'objecte
- Implementació Java completa
- Segon client del patró: l'agregador de restaurants
- Adapter de classe: la variant amb herència
- Adaptadors bidireccionals
- Adapter al JDK
- Quan usar-lo i quan no
- Errors comuns, exercicis i conclusió
El problema a PideYa: l'SDK que no encaixa
Negoci vol acceptar PayPal. L'únic SDK disponible per a l'acord signat és una llibreria veterana, paypal-legacy-sdk, que no podem modificar i la interfície de la qual és aquesta:
// Codi de tercers: NO el podem tocar.
public class PayPalLegacyClient {
/** Cobra una quantia expressada en CENTIMS. Retorna un codi:
* 0 = OK, 1 = fons insuficients, 2 = compte bloquejat, 9 = error tecnic. */
public int makePayment(long amountInCents, String payerAccountId) { /* ... */ }
/** Identificador de l'ultima transaccio realitzada per aquest client. */
public String getLastTransactionId() { /* ... */ }
}El nostre checkout, en canvi, parla l'idioma que vam fixar al mòdul 2 i que ja usen Redsys i Conekta:
public interface PassarellaPagament {
ResultatPagament cobrar(BigDecimal quantia, DadesPagament dades);
}Els desajustos són de tota mena, i són els desajustos típics de qualsevol integració:
| Aspecte | PassarellaPagament (nostra) |
PayPalLegacyClient (d'ells) |
|---|---|---|
| Nom de l'operació | cobrar |
makePayment |
| Quantia | BigDecimal en euros |
long en cèntims |
| Dades del pagador | Objecte DadesPagament |
String amb l'id de compte |
| Resultat | Objecte ResultatPagament (acceptat/rebutjat + motiu) |
Un int amb codis màgics |
| Id de transacció | Dins de ResultatPagament |
Crida a part, getLastTransactionId() |
| Errors | Modelats en el resultat | Codi 9 i a resar |
El primer impuls —escampar conversions pel checkout: if (metode == PAYPAL) { long centims = ...; int codi = client.makePayment(...); if (codi == 0) ... }— destrossa tot el que vam aconseguir al mòdul 2: el client torna a conèixer classes concretes, el switch reneix, i els codis màgics de PayPal contaminen la nostra lògica de negoci. La necessitat, ben formulada: que PayPal sembli una PassarellaPagament més, sense tocar ni l'SDK ni un sol client existent.
Intenció i estructura: Adapter d'objecte
Intenció (GoF): convertir la interfície d'una classe en una altra interfície que els clients esperen. Adapter permet col·laborar a classes que no podrien fer-ho perquè tenen interfícies incompatibles.
La solució canònica és l'Adapter d'objecte: una classe nova que implementa la interfície esperada (Target) i conté l'objecte incompatible (Adaptee), traduint cada crida.
classDiagram
class PassarellaPagament {
<<interface>>
+cobrar(quantia: BigDecimal, dades: DadesPagament) ResultatPagament
}
class AdaptadorPayPal {
-clientPayPal: PayPalLegacyClient
+cobrar(quantia: BigDecimal, dades: DadesPagament) ResultatPagament
}
class PayPalLegacyClient {
+makePayment(amountInCents: long, payerAccountId: String) int
+getLastTransactionId() String
}
class ServeiCheckout
PassarellaPagament <|.. AdaptadorPayPal
AdaptadorPayPal o-- PayPalLegacyClient : hi delega
ServeiCheckout --> PassarellaPagament : usa
Els rols GoF sobre PideYa:
| Rol GoF | A PideYa |
|---|---|
| Target (interfície que el client espera) | PassarellaPagament |
| Adaptee (classe existent incompatible) | PayPalLegacyClient |
| Adapter (traductor) | AdaptadorPayPal |
| Client | ServeiCheckout i qualsevol codi que usi PassarellaPagament |
La geometria és mínima —una classe, una interfície implementada, una referència continguda— però repara exactament la fractura: el client continua monolingüe en PassarellaPagament, i tota la "duana" (unitats, codis, noms) viu en un únic lloc, complint SRP: l'única raó de canvi d'AdaptadorPayPal és que canviï l'SDK o el nostre contracte.
Implementació Java completa
public class AdaptadorPayPal implements PassarellaPagament {
private final PayPalLegacyClient clientPayPal;
public AdaptadorPayPal(PayPalLegacyClient clientPayPal) {
this.clientPayPal = clientPayPal;
}
@Override
public ResultatPagament cobrar(BigDecimal quantia, DadesPagament dades) {
// 1. Traduir les dades d'entrada: euros -> centims, DadesPagament -> id de compte.
long centims = quantia.movePointRight(2)
.setScale(0, RoundingMode.HALF_UP)
.longValueExact();
String compte = dades.getComptePayPal();
// 2. Delegar en l'adaptee.
int codi = clientPayPal.makePayment(centims, compte);
// 3. Traduir el resultat: codis magics -> el nostre model.
return switch (codi) {
case 0 -> ResultatPagament.acceptat(clientPayPal.getLastTransactionId());
case 1 -> ResultatPagament.rebutjat("Fons insuficients");
case 2 -> ResultatPagament.rebutjat("Compte PayPal bloquejat");
default -> ResultatPagament.error("Error tecnic de PayPal (codi " + codi + ")");
};
}
}Tres detalls de l'ofici que separen un adaptador correcte d'un de perillós:
- La conversió d'unitats és sagrada.
movePointRight(2)amblongValueExact()falla sorollosament si la quantia tingués fraccions de cèntim, en lloc de truncar en silenci. En pagaments, un adaptador que arrodoneix malament és un incident comptable; escriu tests específics de la traducció (19,99 € → 1999; 0,105 € → excepció). - La traducció d'errors també és traducció. El codi 9 de l'SDK es converteix en el nostre vocabulari (
ResultatPagament.error(...)). Un adaptador que deixa escapar excepcions o codis de l'adaptee està incomplet: el client acabaria coneixent l'SDK a través dels seus errors. - L'adaptee s'injecta, no es construeix a dins amb
new. Així l'adaptador es testeja amb un doble de l'SDK, i la creació queda on el mòdul 2 va manar: a l'arrel de composició o en una fàbrica.
I la integració amb el que ja tenim construït surt gratis: com que AdaptadorPayPal és una PassarellaPagament, encaixa en la maquinària creacional del mòdul 2 sense tocar-la. Per exemple, si demà el mercat Espanya ofereix PayPal, FabricaEspanya pot retornar-lo des del seu factory method:
@Override
public PassarellaPagament crearPassarellaPagament() {
return switch (config.getMetodePagamentPreferit()) {
case REDSYS -> new PassarellaRedsys(config.getClauRedsys());
case PAYPAL -> new AdaptadorPayPal(new PayPalLegacyClient());
};
}El checkout no s'ha assabentat de res. Aquesta és la prova del cotó d'un bon adaptador: la seva existència és invisible per al client.
Segon client del patró: l'agregador de restaurants
El mateix moviment apareix a l'altra frontera de PideYa: un agregador extern ens cedeix el seu catàleg de restaurants, amb el seu propi model:
// API de l'agregador (tercers): noms en angles, telefons sense normalitzar,
// categories amb la seva propia taxonomia...
public class ExternalRestaurantApi {
public List<ExternalVenue> fetchVenues(String cityCode) { /* ... */ }
}El nostre catàleg parla ProveidorRestaurants (amb List<Restaurant> buscarPerCiutat(Ciutat ciutat)). La solució és idèntica: un AdaptadorAgregador implements ProveidorRestaurants que conté ExternalRestaurantApi i tradueix ExternalVenue → Restaurant (mapatge de camps, normalització de telèfons, conversió de taxonomies). No repetirem el codi: el que importa és reconèixer la firma del context, sempre la mateixa:
- Existeix un contracte nostre contra el qual programa el sistema (Target).
- Arriba una peça que no el compleix i no podem modificar (Adaptee).
- No volem que el desajust contamini els clients.
Quan aquestes tres condicions es donen, Adapter. Quan falta la segona —la peça sí que és nostra i podem canviar-la—, la resposta sol ser més simple: canvia la peça, no li posis un endoll a sobre.
Adapter de classe: la variant amb herència
El GoF cataloga una segona forma, l'Adapter de classe: en lloc de contenir l'adaptee, l'adaptador n'hereta i alhora implementa el target.
classDiagram
class PassarellaPagament { <<interface>> +cobrar(quantia, dades) ResultatPagament }
class PayPalLegacyClient { +makePayment(amountInCents, payerAccountId) int }
class AdaptadorPayPalPerHerencia { +cobrar(quantia, dades) ResultatPagament }
PassarellaPagament <|.. AdaptadorPayPalPerHerencia
PayPalLegacyClient <|-- AdaptadorPayPalPerHerencia : hereta
public class AdaptadorPayPalPerHerencia extends PayPalLegacyClient implements PassarellaPagament {
@Override
public ResultatPagament cobrar(BigDecimal quantia, DadesPagament dades) {
int codi = makePayment(convertirACentims(quantia), dades.getComptePayPal());
// ... mateixa traduccio del resultat
}
}Comparativa honesta, que en Java es decideix gairebé sola:
| Aspecte | Adapter d'objecte (composició) | Adapter de classe (herència) |
|---|---|---|
| Àmbit GoF | Objecte: la relació es fixa en construir | Classe: la relació es fixa en compilar |
| Pot adaptar subclasses de l'adaptee | Sí, qualsevol que li injectin | No: només la classe de la qual hereta |
| Pot adaptar diversos adaptees alhora | Sí (en conté diversos) | No en Java (herència simple) |
| Sobreescriure comportament de l'adaptee | No directament | Sí (n'és una subclasse) |
| Risc | Cap d'especial | Exposa la interfície de l'adaptee al client (makePayment continua sent públic!) |
| Requereix adaptee heretable | No: val interfície, classe final, diverses instàncies | Sí: classe no final amb constructor accessible |
L'última fila de risc mereix subratllat: AdaptadorPayPalPerHerencia és un PayPalLegacyClient, així que qualsevol pot saltar-se la traducció i cridar makePayment directament. L'adaptador d'objecte encapsula; el de classe filtra. Per això —i per l'herència simple de Java, i pel lema "composició sobre herència" de la lliçó de principis— la variant d'objecte és l'elecció per defecte, i la de classe queda per a casos molt concrets (necessites sobreescriure mètodes protegits de l'adaptee, o el rendiment d'una indirecció t'importa de debò, cosa que passa gairebé mai).
Adaptadors bidireccionals
De vegades la traducció cal en els dos sentits. Exemple real de PideYa: durant la migració des d'un sistema antic de repartiment, hi ha mòduls nous que parlen ServeiRepartiment (nostre) i mòduls antics que encara parlen LegacyDispatchSystem (el vell), i tots dos han de conviure mesos. Un adaptador bidireccional implementa les dues interfícies i tradueix en totes dues direccions:
public class AdaptadorRepartimentBidireccional implements ServeiRepartiment, LegacyDispatchSystem {
private final ServeiRepartiment repartimentNou; // per atendre els moduls vells
private final LegacyDispatchSystem repartimentVell; // per atendre els moduls nous
// Crida d'un modul nou -> es tradueix al sistema vell
@Override
public void assignarRepartidor(Comanda comanda, Repartidor repartidor) {
repartimentVell.dispatch(comanda.getId().toString(), repartidor.getCodiLegacy());
}
// Crida d'un modul vell -> es tradueix al sistema nou
@Override
public void dispatch(String orderId, String courierCode) {
repartimentNou.assignarRepartidor(buscarComanda(orderId), buscarRepartidor(courierCode));
}
// ...
}És un patró de transició: útil mentre conviuen dos mons, i candidat a desaparèixer quan la migració acaba. Si un adaptador bidireccional fa anys al teu codi, no és un pont: és una frontera que ningú no s'ha atrevit a tancar.
Adapter al JDK
El JDK és ple d'adaptadors amb nom i cognoms; reconèixer-los fixa el patró millor que qualsevol exemple inventat:
InputStreamReader: l'adaptador més citat de Java. Target:Reader(món de caràcters). Adaptee:InputStream(món de bytes). El constructornew InputStreamReader(inputStream, UTF_8)és literalment "embolcallo un adaptee i el presento com a target", amb el charset com a regla de traducció. El seu simètric ésOutputStreamWriter.Arrays.asList(...): presenta un array (adaptee) darrere la interfícieList(target). La traducció és tan directa que ni tan sols copia les dades.Collections.enumeration(...)/Collections.list(...): parell d'adaptadors entre la vellaEnumerationi les col·leccions modernes — un bidireccional de museu, nascut exactament d'una migració com la de l'apartat anterior.
Quan al javadoc llegeixis "bridge from X to Y" o vegis un constructor que rep "l'altra" interfície, ets gairebé sempre davant d'un Adapter (encara que el javadoc d'InputStreamReader digui bridge, el patró Bridge és una altra cosa, com veurem a la pròxima lliçó).
Quan usar-lo i quan no
Usa'l quan:
- Necessites usar una classe existent —de tercers, llegada, generada— i la seva interfície no coincideix amb la que el teu sistema espera.
- Vols aïllar els teus clients dels detalls (unitats, codis, noms, errors) d'una dependència externa: l'adaptador és a més una capa anticorrupció en miniatura.
- Estàs migrant entre dos sistemes que han de conviure (bidireccional, temporal).
No l'usis quan:
- Les dues classes són teves i pots alinear-ne les interfícies directament: un adaptador entre dues peces pròpies sol ser una refactorització que no et vas atrevir a fer.
- El que vols no és traduir una interfície sinó afegir-hi comportament (això és Decorator) o simplificar moltes crides en una (això és Facade).
- El desajust és tan profund que l'"adaptador" necessita reimplementar mitja lògica: això ja no és traduir, és construir una altra cosa, i mereix un disseny propi.
Relació amb altres patrons (només menció): les fàbriques del mòdul 2 són el lloc natural on decidir si es lliura l'objecte natiu o l'adaptat; Decorator i Proxy comparteixen mecànica d'embolcall però conserven la interfície en lloc de canviar-la; Bridge se li assembla en el dibuix però es dissenya abans que existeixin les peces, no després; i Facade també media amb codi aliè, però simplificant un subsistema sencer en lloc de traduir una classe. L'acarament complet dels quatre embolcalls arriba a la comparativa.
Errors Comuns i Consells
- Adaptador amb lògica de negoci. Si
AdaptadorPayPalcomença a decidir descomptes o a validar cistelles, ha deixat de ser una duana per ser un contrabandista. Regla: en un adaptador només hi ha traducció (tipus, unitats, noms, errors). La resta, a la seva capa. - Traducció incompleta. Adaptar els casos feliços i deixar que les excepcions de l'adaptee travessin crues. El client acaba amb
catch (PayPalConnectionException e)i l'acoblament que volies evitar ha tornat per la porta del darrere. - Adaptar el que és teu. Posar un adaptador a una classe pròpia per no refactoritzar-la és acumular deute amb interessos: ara hi ha dues interfícies i un traductor per mantenir.
- L'adaptee s'escapa. Retornar des de l'adaptador tipus de l'SDK (
ExternalVenue, codisint) en algun mètode secundari. La frontera ha de ser estanca: tot el que creua, es tradueix. - Un adaptador per dominar-los tots. Un
AdaptadorUniversalPagamentsque adapta tres SDKs diferents ambifinterns. Cada adaptee mereix el seu adaptador; la uniformitat ja la dóna el target. - Consell: anomena els adaptadors de forma transparent (
AdaptadorPayPal, sufixAdapter/Adaptador). A diferència d'altres patrons, aquí anunciar el patró en el nom ajuda: qui ho llegeixi sabrà que a dins només hi ha traducció i on buscar la duana de cada tercer.
Exercicis
Exercici 1: adaptar el servei de SMS d'un nou proveïdor
PideYa contracta un proveïdor de SMS més barat l'SDK del qual (intocable) és:
public class CheapSmsService {
/** Retorna true si el missatge ha estat encuat. El numero ha d'anar SENSE prefix internacional. */
public boolean queueMessage(String phoneWithoutPrefix, String body) { /* ... */ }
}La nostra interfície del mòdul 2 és Notificador amb void enviar(String destinatari, String missatge), on destinatari és un telèfon amb prefix (+34600111222), i una fallada d'enviament es comunica llançant NotificacioException. Escriu AdaptadorCheapSms.
Exercici 2: detectar l'adaptador defectuós
Quins dos problemes de disseny té aquest adaptador de l'agregador de restaurants?
public class AdaptadorAgregador implements ProveidorRestaurants {
private final ExternalRestaurantApi api = new ExternalRestaurantApi();
@Override
public List<Restaurant> buscarPerCiutat(Ciutat ciutat) {
List<ExternalVenue> venues = api.fetchVenues(ciutat.getCodi());
List<Restaurant> resultat = new ArrayList<>();
for (ExternalVenue v : venues) {
if (v.getRating() >= 4.0) { // "nomes volem restaurants bons"
resultat.add(traduir(v));
}
}
return resultat;
}
}Exercici 3: objecte o classe?
L'SDK PayPalLegacyClient té un mètode protected void refreshToken() que convé invocar abans de cada cobrament, i la classe no és final. Un company proposa l'Adapter de classe "perquè així podem cridar refreshToken()". Avalua la proposta: és raó suficient? Quins inconvenients accepta a canvi? Hi ha alternativa mantenint la variant d'objecte?
Solucions
Solució 1:
public class AdaptadorCheapSms implements Notificador {
private final CheapSmsService servei;
public AdaptadorCheapSms(CheapSmsService servei) {
this.servei = servei;
}
@Override
public void enviar(String destinatari, String missatge) {
// Traduccio d'entrada: treure el prefix internacional (+34 -> "")
String sensePrefix = destinatari.replaceFirst("^\\+\\d{1,3}", "");
// Delegacio + traduccio del resultat: boolean -> excepcio del nostre vocabulari
boolean encuat = servei.queueMessage(sensePrefix, missatge);
if (!encuat) {
throw new NotificacioException("El proveidor de SMS ha rebutjat el missatge a " + destinatari);
}
}
}Les dues traduccions (format del telèfon i model d'error) queden tancades dins l'adaptador; per al RegistreNotificadors del mòdul 2, registrar aquest canal és una línia més: registre.registrar("sms-barat", () -> new AdaptadorCheapSms(new CheapSmsService()));.
Solució 2: (1) construeix el seu adaptee amb new a dins (new ExternalRestaurantApi()): impossible de testejar sense xarxa i creació fora de lloc; s'ha d'injectar. (2) Contraban de lògica de negoci: el filtre rating >= 4.0 és una regla de catàleg, no una traducció; demà negoci la canviarà i ningú no mirarà dins d'un adaptador. El filtratge pertany al servei de catàleg que usa el proveïdor. (Traduir rating al nostre model sí que seria feina de l'adaptador; decidir amb ell, no.)
Solució 3: no és raó suficient per si sola, però és una raó legítima: accedir a membres protected és un dels pocs motius reals de l'Adapter de classe. A canvi accepta: la interfície pública de l'SDK queda exposada a l'adaptador (qualsevol pot cridar makePayment sense traducció), la relació queda soldada a aquella classe concreta, i es gasta l'única carta d'herència de Java. Alternativa mantenint objecte: una subclasse mínima de l'SDK (PayPalClientAmbRefresh extends PayPalLegacyClient) que només publiqui cobrarAmbTokenFresc() combinant refreshToken() + makePayment(...), i l'adaptador d'objecte normal embolcallant-la. L'herència queda confinada a un detall tècnic i la frontera continua estanca.
Conclusió
Adapter resol el desencontre entre la interfície que el teu sistema espera i la que una peça intocable ofereix: una classe traductora que implementa el target, conté l'adaptee i converteix crides, dades i errors en un únic punt. Coneixes les seves dues variants (objecte per defecte, classe per a casos comptats), la seva versió bidireccional per a migracions, i els seus exemplars de museu al JDK. I tens el seu criteri de puresa: dins d'un adaptador només hi viu traducció.
Fixa't que Adapter arriba sempre tard i d'urgències: les peces ja existien i no encaixaven. La pregunta següent és més ambiciosa: i si poguéssim dissenyar per endavant perquè dues dimensions que variaran —què es comunica i per on es comunica— no arribin mai a soldar-se? A PideYa aquesta tensió ja treu el cap: tipus de notificació per una banda, canals d'enviament per l'altra, i una jerarquia que amenaça de multiplicar-se. Aquest disseny a priori té nom d'obra civil: ens veiem a Bridge.
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
