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
- El problema a PideYa: l'expansió internacional
- Intenció i estructura del patró
- Implementació Java completa
- Afegir un mercat i afegir un producte: l'asimetria clau
- Relació i diferència amb Factory Method
- Quan usar-lo i quan no
- 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 afegircrearValidadorAdrecaFiscal()a la interfícieFabricaMercat... 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í: é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
ServeiCheckoutapareix unif (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
FabricaMercatproductes 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
FabricaDeProvesque 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, noFabricaConekta): 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:
- Crear l'objecte
InformeVendesadequat (PDF, Excel, HTML) segons el que demani el restaurant. - Els repartidors de flota pròpia i els autònoms requereixen, cada règim, el seu
Contracte, la sevaPolissaAssegurancai la sevaCalculadoraLiquidacio, sempre del mateix règim. - 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:
- Factory Method (o fins i tot simple factory): un sol producte (
InformeVendes), sense coherència amb altres objectes. - 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.
- 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
- 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
