A la lliçó anterior vam aprendre a fabricar productes d'un en un sense acoblar el client a les seves classes. Avui pugem un nivell: hi ha problemes on els objectes no viatgen sols sinó en famílies que han de ser coherents entre si, i crear una peça de la família equivocada és un bug —de vegades caríssim—. És exactament el que li va passar a PideYa en expandir-se a Mèxic: una passarel·la de pagament mexicana combinada amb una calculadora d'IVA espanyol. Abstract Factory existeix perquè aquesta barreja sigui impossible per construcció.

Contingut

  1. El problema a PideYa: l'expansió internacional
  2. Intenció i estructura del patró
  3. Implementació Java completa
  4. Afegir un mercat i afegir un producte: l'asimetria clau
  5. Relació i diferència amb Factory Method
  6. Quan usar-lo i quan no
  7. Errors comuns, exercicis i conclusió

El problema a PideYa: l'expansió internacional

PideYa obre operacions a Mèxic. Cada mercat necessita la seva pròpia versió de tres peces que participen en tot cobrament:

Peça Espanya Mèxic
Passarel·la de pagament Redsys (targetes, Bizum) Conekta (targetes, OXXO)
Calculadora d'impostos IVA 10% menjar a domicili IVA 16% general
Formatador de tiquets Normativa espanyola (NIF, "IVA") Normativa mexicana (RFC, "IVA", llegenda CFDI)

Les tres peces ja tenen les seves interfícies (PassarellaPagament, CalculadoraImpostos, FormatadorTiquet) i el checkout programa contra elles. El primer intent d'internacionalització creava cada peça per separat:

// Primer intent: cada peca es tria pel seu compte. NO imitar.
PassarellaPagament passarella = switch (pais) {
    case ES -> new PassarellaRedsys(config.getClauRedsys());
    case MX -> new PassarellaConekta(config.getClauConekta());
};
CalculadoraImpostos impostos = switch (pais) {
    case ES -> new ImpostosEspanya();
    case MX -> new ImpostosMexic();
};
FormatadorTiquet tiquet = switch (pais) {
    case ES -> new TiquetEspanya();
    case MX -> new TiquetMexic();
};

A més de triplicar el switch (dolor conegut de la lliçó anterior), aquest codi té un defecte més greu i més subtil: la coherència de la família depèn de la disciplina humana. Res no impedeix que un refactor despistat, una branca mal fusionada o un copia-enganxa deixi PassarellaConekta convivint amb ImpostosEspanya. Va ser literalment l'incident de producció: cobraments a Mèxic amb un 10% d'IVA espanyol. El compilador no va protestar, perquè cada peça era vàlida per separat; l'invàlid era la combinació, i cap tipus no la representava.

La necessitat, formulada amb precisió: volem crear la família completa d'una vegada, garantint que totes les peces pertanyen al mateix mercat, i que el checkout no sàpiga mai quin mercat està servint.

Intenció i estructura del patró

Intenció (GoF): proporcionar una interfície per crear famílies d'objectes relacionats o dependents sense especificar les seves classes concretes.

La solució: una interfície de fàbrica amb un mètode de creació per cada tipus de producte de la família. Cada implementació de la fàbrica correspon a una variant (un mercat) i fabrica tots els productes d'aquesta variant. El client rep una fàbrica i li demana totes les peces: la coherència queda garantida perquè una FabricaMexic és físicament incapaç de produir impostos espanyols.

classDiagram
    class FabricaMercat {
        <<interface>>
        +crearPassarellaPagament() PassarellaPagament
        +crearCalculadoraImpostos() CalculadoraImpostos
        +crearFormatadorTiquet() FormatadorTiquet
    }
    class FabricaEspanya {
        +crearPassarellaPagament() PassarellaPagament
        +crearCalculadoraImpostos() CalculadoraImpostos
        +crearFormatadorTiquet() FormatadorTiquet
    }
    class FabricaMexic {
        +crearPassarellaPagament() PassarellaPagament
        +crearCalculadoraImpostos() CalculadoraImpostos
        +crearFormatadorTiquet() FormatadorTiquet
    }
    class PassarellaPagament { <<interface>> }
    class CalculadoraImpostos { <<interface>> }
    class FormatadorTiquet { <<interface>> }
    class PassarellaRedsys
    class PassarellaConekta
    class ImpostosEspanya
    class ImpostosMexic
    class TiquetEspanya
    class TiquetMexic
    class ServeiCheckout

    FabricaMercat <|.. FabricaEspanya
    FabricaMercat <|.. FabricaMexic
    PassarellaPagament <|.. PassarellaRedsys
    PassarellaPagament <|.. PassarellaConekta
    CalculadoraImpostos <|.. ImpostosEspanya
    CalculadoraImpostos <|.. ImpostosMexic
    FormatadorTiquet <|.. TiquetEspanya
    FormatadorTiquet <|.. TiquetMexic
    FabricaEspanya ..> PassarellaRedsys : crea
    FabricaEspanya ..> ImpostosEspanya : crea
    FabricaEspanya ..> TiquetEspanya : crea
    FabricaMexic ..> PassarellaConekta : crea
    FabricaMexic ..> ImpostosMexic : crea
    FabricaMexic ..> TiquetMexic : crea
    ServeiCheckout --> FabricaMercat : usa

Els rols GoF sobre PideYa:

Rol GoF A PideYa
AbstractFactory FabricaMercat
ConcreteFactory (una per variant) FabricaEspanya, FabricaMexic
AbstractProduct (un per tipus de peça) PassarellaPagament, CalculadoraImpostos, FormatadorTiquet
ConcreteProduct PassarellaRedsys, ImpostosMexic, TiquetEspanya, ...
Client ServeiCheckout (només coneix interfícies)

Observa la geometria, que és la firma visual del patró: una matriu de variants × productes. Les files són mercats; les columnes, tipus de producte; cada fàbrica concreta materialitza una fila completa. I a diferència de Factory Method, aquí la fàbrica és un objecte que es passa i s'injecta: àmbit d'objecte, composició per sobre d'herència.

Implementació Java completa

Productes abstractes (dos dels tres, per brevetat; FormatadorTiquet és anàleg):

public interface PassarellaPagament {
    ResultatPagament cobrar(BigDecimal quantia, DadesPagament dades);
}

public interface CalculadoraImpostos {
    /** Retorna la quantia de l'impost per a una base imposable donada. */
    BigDecimal calcular(BigDecimal baseImposable);
}

Productes concrets de cada mercat:

public class PassarellaRedsys implements PassarellaPagament {
    private final String clauComerc;

    public PassarellaRedsys(String clauComerc) { this.clauComerc = clauComerc; }

    @Override
    public ResultatPagament cobrar(BigDecimal quantia, DadesPagament dades) {
        // Integracio real amb Redsys: signatura, TPV virtual, Bizum...
        return ResultatPagament.acceptat("redsys-" + UUID.randomUUID());
    }
}

public class ImpostosEspanya implements CalculadoraImpostos {
    private static final BigDecimal IVA_MENJAR_DOMICILI = new BigDecimal("0.10");

    @Override
    public BigDecimal calcular(BigDecimal base) {
        return base.multiply(IVA_MENJAR_DOMICILI).setScale(2, RoundingMode.HALF_UP);
    }
}

public class PassarellaConekta implements PassarellaPagament {
    private final String apiKey;

    public PassarellaConekta(String apiKey) { this.apiKey = apiKey; }

    @Override
    public ResultatPagament cobrar(BigDecimal quantia, DadesPagament dades) {
        // Integracio amb Conekta: targetes, pagaments en efectiu via OXXO...
        return ResultatPagament.acceptat("conekta-" + UUID.randomUUID());
    }
}

public class ImpostosMexic implements CalculadoraImpostos {
    private static final BigDecimal IVA_GENERAL = new BigDecimal("0.16");

    @Override
    public BigDecimal calcular(BigDecimal base) {
        return base.multiply(IVA_GENERAL).setScale(2, RoundingMode.HALF_UP);
    }
}

La fàbrica abstracta i les concretes —el cor del patró—:

public interface FabricaMercat {
    PassarellaPagament crearPassarellaPagament();
    CalculadoraImpostos crearCalculadoraImpostos();
    FormatadorTiquet crearFormatadorTiquet();
}

public class FabricaEspanya implements FabricaMercat {
    private final ConfiguracioPideYa config;

    public FabricaEspanya(ConfiguracioPideYa config) { this.config = config; }

    @Override
    public PassarellaPagament crearPassarellaPagament() {
        return new PassarellaRedsys(config.getClauRedsys());
    }

    @Override
    public CalculadoraImpostos crearCalculadoraImpostos() {
        return new ImpostosEspanya();
    }

    @Override
    public FormatadorTiquet crearFormatadorTiquet() {
        return new TiquetEspanya();
    }
}

public class FabricaMexic implements FabricaMercat {
    private final ConfiguracioPideYa config;

    public FabricaMexic(ConfiguracioPideYa config) { this.config = config; }

    @Override
    public PassarellaPagament crearPassarellaPagament() {
        return new PassarellaConekta(config.getClauConekta());
    }

    @Override
    public CalculadoraImpostos crearCalculadoraImpostos() {
        return new ImpostosMexic();
    }

    @Override
    public FormatadorTiquet crearFormatadorTiquet() {
        return new TiquetMexic();
    }
}

Fixa't que cada mètode de la fàbrica és un factory method (en el sentit ampli): Abstract Factory és, en la seva implementació més comuna, un paquet de factory methods agrupats per variant. D'aquí que el GoF digui que tots dos patrons solen aparèixer junts.

El client: rep la fàbrica injectada i no esmenta cap mercat, cap classe concreta, mai:

public class ServeiCheckout {

    private final PassarellaPagament passarella;
    private final CalculadoraImpostos impostos;
    private final FormatadorTiquet formatador;

    /** Tota la familia surt de LA MATEIXA fabrica: coherencia per construccio. */
    public ServeiCheckout(FabricaMercat fabrica) {
        this.passarella = fabrica.crearPassarellaPagament();
        this.impostos = fabrica.crearCalculadoraImpostos();
        this.formatador = fabrica.crearFormatadorTiquet();
    }

    public Tiquet confirmarComanda(Comanda comanda) {
        BigDecimal base = comanda.getTotalSenseImpostos();
        BigDecimal impost = impostos.calcular(base);
        BigDecimal total = base.add(impost);

        ResultatPagament resultat = passarella.cobrar(total, comanda.getDadesPagament());
        if (!resultat.esAcceptat()) {
            throw new PagamentRebutjatException(resultat.getMotiu());
        }
        return formatador.formatar(comanda, base, impost, total);
    }
}

L'arrel de composició, únic lloc on el mercat es decideix (nota que és el mateix moviment que vam aprendre amb els notificadors: un punt únic de decisió, aquí elevat de "quin producte" a "quina família"):

FabricaMercat fabrica = switch (mercat) {
    case ES -> new FabricaEspanya(config);
    case MX -> new FabricaMexic(config);
};
ServeiCheckout checkout = new ServeiCheckout(fabrica);

Ara la barreja que va causar l'incident és impossible d'escriure: no existeix cap seqüència de crides que combini Conekta amb impostos espanyols, perquè les peces ja no es trien una a una. La invariant "tot del mateix mercat" ha passat de ser una convenció a ser una propietat del sistema de tipus. I de regal: testejar el checkout és trivial amb una FabricaDeProves que retorni dobles coherents.

Afegir un mercat i afegir un producte: l'asimetria clau

Tota decisió de disseny compra flexibilitat en un eix pagant-la en un altre. La d'Abstract Factory és nítida i l'has de conèixer abans d'adoptar-lo:

  • Afegir una variant (fila) és barat i net. França: FabricaFranca + PassarellaStripeFranca + ImpostosFranca + TiquetFranca. Tot són classes noves; ni el client ni les fàbriques existents es toquen. OCP perfecte en l'eix "mercats".
  • Afegir un tipus de producte (columna) és car. Si cada mercat necessita ara un ValidadorAdrecaFiscal, cal afegir crearValidadorAdrecaFiscal() a la interfície FabricaMercat... i això trenca totes les fàbriques concretes existents, que l'han d'implementar. És una modificació en cascada, el preu estructural del patró.

Regla pràctica: Abstract Factory encaixa quan el conjunt de productes de la família és estable i el que creix són les variants. Si esperes el contrari (productes nous sovint, variants fixes), el patró et farà patir; considera fàbriques més petites o interfícies segregades (l'esperit d'ISP de la lliçó de principis).

Relació i diferència amb Factory Method

És la confusió més freqüent de tot el mòdul; deixem-la resolta amb una taula:

Aspecte Factory Method Abstract Factory
Fabrica Un producte Una família de productes relacionats
Mecanisme Un mètode (sovint heretable) dins d'una classe amb lògica pròpia Un objecte dedicat exclusivament a fabricar, amb un mètode per producte
Àmbit GoF Classe (l'herència decideix el producte) Objecte (s'injecta la fàbrica; composició)
Garanteix coherència entre productes No aplica (només n'hi ha un) : és la seva raó de ser
Intenció dominant Obrir un punt d'extensió Blindar una combinació vàlida
Relació mútua Els seus mètodes solen implementar-se com a factory methods

Dues frases per endur-se: Factory Method és un mètode; Abstract Factory és un objecte. I: si els teus productes no necessiten ser coherents entre si, no tens una família: tens productes solts, i amb Factory Method (o diversos) en tens prou. L'evolució natural de l'un a l'altre la traçarem a la comparativa del mòdul.

Quan usar-lo i quan no

Usa'l quan:

  • El sistema ha de funcionar amb diverses famílies intercanviables de productes i la coherència intrafamília és una invariant de negoci (mercats de PideYa; l'exemple GoF clàssic: look and feel d'interfícies gràfiques, amb botons i menús que han de ser tots del mateix estil).
  • Vols poder canviar la família sencera en un sol punt (configuració, arrencada, fins i tot en calent).
  • Vols blindar per tipus que "les peces d'A no es barregen amb les de B".

No l'usis quan:

  • Només hi ha una família (un mercat): és la sobreenginyeria catalogada a la lliçó de la balança; el dia que arribi el segon mercat, refactoritzar cap al patró serà natural.
  • Els "productes" no guarden relació de coherència: són fàbriques independents disfressades de família.
  • El catàleg de productes de la família canvia sovint (l'asimetria de la secció anterior et castigarà).

Relació amb altres patrons (només menció): les fàbriques concretes no solen tenir estat i sovint es comparteixen com a Singleton; una fàbrica es pot implementar internament amb Prototype (clonant exemplars en comptes d'instanciar); i els productes complexos que la fàbrica retorna es poden construir per dins amb un Builder.

Errors Comuns i Consells

  • Filtrar el mercat cap al client. Si dins de ServeiCheckout apareix un if (pais == MX), el patró ha fracassat: tota variació per mercat ha de viure als productes concrets o a la seva fàbrica. El client ha de ser monolingüe en interfícies.
  • La fàbrica "calaix de sastre". Ficar a FabricaMercat productes sense relació de coherència (crearLoggerDeComandes()?) només perquè "ja tenim la fàbrica a mà". Cada mètode nou encareix la interfície per a totes les variants; la família ha de tenir un criteri de pertinença clar.
  • Explosió combinatòria silenciosa. Amb 5 mercats i 4 productes hi ha 20 classes concretes + 5 fàbriques. És el cost honest del patró; si diverses variants comparteixen peces (Mèxic i Colòmbia usen la mateixa passarel·la), extreu classes base o compon-les: les fàbriques poden retornar instàncies compartides.
  • Oblidar la fàbrica de proves. Una FabricaDeProves que retorna dobles coherents és un dels regals més grans del patró per als tests; si no l'escrius, estàs pagant el patró sense cobrar una de les seves rendes.
  • Consell: anomena les fàbriques per la variant, no per la tecnologia (FabricaMexic, no FabricaConekta): la tecnologia és un detall que pot canviar dins de la mateixa variant sense tocar ningú.

Exercicis

Exercici 1: afegir el mercat França

Afegeix França a la implementació de la lliçó: passarel·la Stripe (necessita apiKeyStripe de configuració), IVA del 10% per a menjar a domicili i tiquet amb normativa francesa (SIRET). Escriu la fàbrica concreta i enumera els fitxers modificats.

Exercici 2: detectar la família trencada

Un company proposa aquesta "millora" per estalviar classes. Quina garantia del patró destrueix i quin error concret torna a ser possible?

public class FabricaFlexible implements FabricaMercat {
    private final Pais paisPassarella;
    private final Pais paisImpostos;
    private final Pais paisTiquet;

    public FabricaFlexible(Pais paisPassarella, Pais paisImpostos, Pais paisTiquet) { /*...*/ }

    @Override
    public PassarellaPagament crearPassarellaPagament() {
        return paisPassarella == Pais.ES ? new PassarellaRedsys(...) : new PassarellaConekta(...);
    }
    // ... analeg per a impostos i tiquet, cadascun amb el SEU pais
}

Exercici 3: Factory Method o Abstract Factory?

Per a cada situació de PideYa, decideix quin dels dos patrons encaixa i justifica-ho en una frase:

  1. Crear l'objecte InformeVendes adequat (PDF, Excel, HTML) segons el que demani el restaurant.
  2. Els repartidors de flota pròpia i els autònoms requereixen, cada règim, el seu Contracte, la seva PolissaAsseguranca i la seva CalculadoraLiquidacio, sempre del mateix règim.
  3. Cada tipus de promoció (2x1, enviament gratis, descompte percentual) necessita crear el seu propi objecte ReglaValidacio.

Solucions

Solució 1:

public class PassarellaStripe implements PassarellaPagament { /* cobrar amb apiKeyStripe */ }
public class ImpostosFranca implements CalculadoraImpostos {
    private static final BigDecimal TVA_MENJAR = new BigDecimal("0.10");
    @Override
    public BigDecimal calcular(BigDecimal base) {
        return base.multiply(TVA_MENJAR).setScale(2, RoundingMode.HALF_UP);
    }
}
public class TiquetFranca implements FormatadorTiquet { /* SIRET, mentions legales */ }

public class FabricaFranca implements FabricaMercat {
    private final ConfiguracioPideYa config;
    public FabricaFranca(ConfiguracioPideYa config) { this.config = config; }
    @Override public PassarellaPagament crearPassarellaPagament() { return new PassarellaStripe(config.getApiKeyStripe()); }
    @Override public CalculadoraImpostos crearCalculadoraImpostos() { return new ImpostosFranca(); }
    @Override public FormatadorTiquet crearFormatadorTiquet() { return new TiquetFranca(); }
}

Fitxers modificats: només l'arrel de composició (el switch d'arrencada guanya el cas FR). Client, interfícies i fàbriques existents: intactes.

Solució 2: destrueix la invariant de coherència de família, que és l'única raó d'existir del patró. En parametritzar cada producte amb el seu propi país, torna a ser expressable la combinació PassarellaConekta + ImpostosEspanya (n'hi ha prou de construir new FabricaFlexible(MX, ES, MX)): exactament l'incident de producció que va motivar el patró, ara amb més cerimònia. És una Abstract Factory de façana amb la seguretat de tres switch solts.

Solució 3:

  1. Factory Method (o fins i tot simple factory): un sol producte (InformeVendes), sense coherència amb altres objectes.
  2. Abstract Factory: tres productes per règim que han de ser mútuament coherents (contracte d'autònom amb assegurança de flota seria un problema legal): família de manual.
  3. Factory Method: cada promoció (creador) fabrica el seu producte (ReglaValidacio); un producte per creador, sense famílies.

Conclusió

Abstract Factory eleva el moviment de la lliçó anterior del producte a la família: una interfície amb un mètode de creació per peça, una implementació per variant, i la garantia —per tipus, no per disciplina— que les peces mai no es barregen entre variants. Has vist la seva geometria de matriu (variants × productes), la seva asimetria de costos (variant nova barata, producte nou car) i la frontera amb Factory Method: un mètode davant d'un objecte, un producte davant d'una família coherent.

Els dos patrons de fàbrica resolen quina classe instanciar. El següent repte és diferent: de vegades la classe és claríssima, i l'endimoniat és el procés de muntar-la, amb deu paràmetres, la meitat opcionals, i regles de validació creuades. És la història del constructor telescòpic de Comanda que vam deixar pendent a la introducció, i es resol construint pas a pas: ens veiem a Builder.

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