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

  1. El problema a PideYa: l'SDK que no encaixa
  2. Intenció i estructura: Adapter d'objecte
  3. Implementació Java completa
  4. Segon client del patró: l'agregador de restaurants
  5. Adapter de classe: la variant amb herència
  6. Adaptadors bidireccionals
  7. Adapter al JDK
  8. Quan usar-lo i quan no
  9. 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) amb longValueExact() 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:

  1. Existeix un contracte nostre contra el qual programa el sistema (Target).
  2. Arriba una peça que no el compleix i no podem modificar (Adaptee).
  3. 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 constructor new 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 és OutputStreamWriter.
  • Arrays.asList(...): presenta un array (adaptee) darrere la interfície List (target). La traducció és tan directa que ni tan sols copia les dades.
  • Collections.enumeration(...) / Collections.list(...): parell d'adaptadors entre la vella Enumeration i 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 AdaptadorPayPal començ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, codis int) en algun mètode secundari. La frontera ha de ser estanca: tot el que creua, es tradueix.
  • Un adaptador per dominar-los tots. Un AdaptadorUniversalPagaments que adapta tres SDKs diferents amb if interns. Cada adaptee mereix el seu adaptador; la uniformitat ja la dóna el target.
  • Consell: anomena els adaptadors de forma transparent (AdaptadorPayPal, sufix Adapter/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

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