Les despeses d'enviament de PideYa es calculen de tres maneres: per distància als mercats madurs, tarifa plana on estem entrant, i gratis quan aplica una promoció. I la CentralRepartiment va deixar un deute semblant: el criteri d'assignació de repartidor (el més proper?, el menys carregat?, per torns?) estava cablejat dins del mediador. Dos símptomes del mateix mal: algorismes alternatius enterrats en condicionals dins de qui els usa. Strategy és la resposta canònica — i segurament el patró de comportament que més vegades aplicaràs a la teva carrera: una família d'algorismes, cadascun a la seva classe, intercanviables rere una interfície comuna, triats des de fora. Si el curs sencer s'hagués de resumir en un patró, seria aquest: és "programa contra interfícies" i OCP en estat pur.
Contingut
- El problema a PideYa: algorismes enterrats en condicionals
- Intenció i estructura del patró
- Implementació Java completa: despeses d'enviament
- Segon cas: assignació de repartidor
- Seleccionar l'estratègia: config, context i registre
- Strategy amb lambdes i
Function - Strategy vs. State vs. Bridge: la taula
- Quan usar-lo i quan no
- Relació amb altres patrons
- Errors comuns
- Exercicis i conclusió
El problema a PideYa: algorismes enterrats en condicionals
El càlcul d'enviament, versió actual, dins de la FacanaCheckout:
private BigDecimal calcularEnviament(Comanda comanda) {
if (promocioEnviamentGratisActiva && comanda.getTotal().compareTo(MINIM_PROMO) >= 0) {
return BigDecimal.ZERO;
} else if (mercat.equals("MX")) { // mercat nou: plana
return TARIFA_PLANA_MX;
} else {
double km = geo.distancia(comanda.getRestaurant(), comanda.getAdrecaLliurament());
return BASE.add(PER_KM.multiply(BigDecimal.valueOf(km)));
}
}Els mals, ja familiars però amb matís propi:
- Cada política nova modifica el checkout (adeu OCP): la de "tarifa reduïda en hora vall" que demana negoci significa un altre
else ifen una classe que no va d'enviaments. - Les polítiques no es poden provar aïllades: testejar la fórmula per distància arrossega la façana sencera amb les seves cinc dependències.
- No es poden combinar ni configurar: Espanya vol distància, Mèxic plana, i màrqueting vol activar "gratis" per campanya i per ciutat — amb
ifs, cada combinació són més branques. - L'algorisme no és un concepte del codi: "política d'enviament" existeix a les converses de negoci però no hi ha cap classe que es digui així. Senyal clàssic d'abstracció absent.
La jugada, la de tot el mòdul: reïficar. Que cada manera de calcular sigui un objecte amb interfície comuna, i que el checkout depengui només de la interfície.
Intenció i estructura del patró
Intenció (GoF): definir una família d'algorismes, encapsular cadascun, i fer-los intercanviables. Strategy permet que l'algorisme variï independentment dels clients que l'usen.
classDiagram
class CalculEnviament {
<<interface>>
+calcular(comanda: Comanda) BigDecimal
}
class EnviamentPerDistancia {
-geo: ServeiGeo
+calcular(comanda) BigDecimal
}
class EnviamentTarifaPlana {
-tarifa: BigDecimal
+calcular(comanda) BigDecimal
}
class EnviamentGratisPromocio {
+calcular(comanda) BigDecimal
}
class FacanaCheckout {
-calculEnviament: CalculEnviament
+confirmarComanda(...)
}
CalculEnviament <|.. EnviamentPerDistancia
CalculEnviament <|.. EnviamentTarifaPlana
CalculEnviament <|.. EnviamentGratisPromocio
FacanaCheckout o-- CalculEnviament : usa la injectada
| Rol GoF | A PideYa |
|---|---|
| Strategy (interfície de l'algorisme) | CalculEnviament |
| ConcreteStrategy | EnviamentPerDistancia, EnviamentTarifaPlana, EnviamentGratisPromocio |
| Context (usa una estratègia a través de la interfície) | FacanaCheckout |
Estructuralment és el patró més simple del mòdul — interfície, implementacions, camp. La seva profunditat és en les decisions de contorn: quina firma donar a la interfície, qui tria l'estratègia i quan. Cap allà anem.
Implementació Java completa: despeses d'enviament
La interfície. La decisió clau és la firma: ha de donar a totes les estratègies el que necessiten, sense acoblar-les al que no. Comanda com a paràmetre és bon terme mitjà: la de distància llegirà adreces, la plana ho ignorarà gairebé tot:
Les estratègies. Cadascuna amb les seves dependències pròpies, injectades — la de distància necessita el servei geogràfic; les altres ni saben que existeix:
public class EnviamentPerDistancia implements CalculEnviament {
private final ServeiGeo geo;
private final BigDecimal base;
private final BigDecimal perKm;
public EnviamentPerDistancia(ServeiGeo geo, BigDecimal base, BigDecimal perKm) {
this.geo = geo;
this.base = base;
this.perKm = perKm;
}
@Override
public BigDecimal calcular(Comanda comanda) {
double km = geo.distancia(comanda.getRestaurant().getAdreca(),
comanda.getAdrecaLliurament());
return base.add(perKm.multiply(BigDecimal.valueOf(km)))
.setScale(2, RoundingMode.HALF_UP);
}
}
public class EnviamentTarifaPlana implements CalculEnviament {
private final BigDecimal tarifa;
public EnviamentTarifaPlana(BigDecimal tarifa) { this.tarifa = tarifa; }
@Override
public BigDecimal calcular(Comanda comanda) { return tarifa; }
}
public class EnviamentGratisPromocio implements CalculEnviament {
@Override
public BigDecimal calcular(Comanda comanda) { return BigDecimal.ZERO; }
}El context, que ja només coneix la interfície:
public class FacanaCheckout {
private final CalculEnviament calculEnviament; // injectada (DIP)
// ... resta de collaboradors del modul 3 ...
public ConfirmacioComanda confirmarComanda(Cistella cistella, ...) {
// validar (la cadena de 04-02) → impostos → ...
BigDecimal enviament = calculEnviament.calcular(comandaEnCurs);
// ... cobrar, construir amb Comanda.Builder, notificar ...
}
}Cada estratègia es testeja sola en tres línies; la d'"hora vall" serà una classe nova sense tocar res; i "política d'enviament" ja és un concepte amb nom al codi.
Segon cas: assignació de repartidor
El deute del Mediator, saldat. Fixa't que aquí la firma natural rep candidats i comanda — l'estratègia decideix, la central executa:
public interface EstrategiaAssignacio {
Optional<Repartidor> triar(List<Repartidor> disponibles, Comanda comanda);
}
public class RepartidorMesProper implements EstrategiaAssignacio {
private final ServeiGeo geo;
public RepartidorMesProper(ServeiGeo geo) { this.geo = geo; }
@Override
public Optional<Repartidor> triar(List<Repartidor> disponibles, Comanda comanda) {
return disponibles.stream()
.min(Comparator.comparingDouble(
r -> geo.distancia(r.getPosicio(), comanda.getRestaurant().getAdreca())));
}
}
public class RepartidorMenysCarregat implements EstrategiaAssignacio {
@Override
public Optional<Repartidor> triar(List<Repartidor> disponibles, Comanda comanda) {
return disponibles.stream()
.min(Comparator.comparingInt(Repartidor::getLliuramentsAvui));
}
}
public class AssignacioRoundRobin implements EstrategiaAssignacio {
private final AtomicInteger torn = new AtomicInteger(); // estrategia AMB estat!
@Override
public Optional<Repartidor> triar(List<Repartidor> disponibles, Comanda comanda) {
if (disponibles.isEmpty()) return Optional.empty();
return Optional.of(disponibles.get(torn.getAndIncrement() % disponibles.size()));
}
}I la CentralRepartiment substitueix el seu cercarDisponible per estrategia.triar(candidats, comanda) — el mediador orquestra, l'estratègia decideix, tal com vam prometre a la seva lliçó. El round-robin il·lustra un matís important: les estratègies poden tenir estat propi (el torn); quan en tenen, compte a compartir la instància entre contexts que no hagin de compartir aquell estat.
Seleccionar l'estratègia: config, context i registre
Qui tria l'estratègia? És la pregunta del patró, amb tres respostes escalonades:
1. Per configuració (composition root). La més comuna: en arrencar, segons mercat — recorda l'Abstract Factory: la família per mercat pot fabricar també l'estratègia d'enviament, al costat de la passarel·la i els impostos:
CalculEnviament enviament = switch (ConfiguracioPideYa.getInstancia().getMercat()) {
case "ES" -> new EnviamentPerDistancia(geo, new BigDecimal("1.50"), new BigDecimal("0.80"));
case "MX" -> new EnviamentTarifaPlana(new BigDecimal("35.00"));
default -> throw new IllegalStateException();
};(Sí, hi ha un switch — però un de sol, a la vora de l'aplicació, no repetit a cada punt d'ús. Aquest és el tracte.)
2. Per context de la petició, en execució. Si amb promoció activa l'enviament és gratis, algú decideix per comanda. Aquest "algú" pot ser un petit selector — que és, alhora, una estratègia d'ordre superior:
public class SelectorEnviament implements CalculEnviament {
private final ServeiPromocions promos;
private final CalculEnviament normal;
@Override
public BigDecimal calcular(Comanda comanda) {
return promos.teEnviamentGratis(comanda)
? BigDecimal.ZERO
: normal.calcular(comanda);
}
}3. Per registre, estil RegistreNotificadors. El mateix moviment del Factory Method amb lambdes: un mapa nom → estratègia, i l'elecció es torna dada (configurable en calent, ampliable sense tocar el selector):
public class RegistreEstrategiesEnviament {
private final Map<String, CalculEnviament> estrategies = new HashMap<>();
public void registrar(String nom, CalculEnviament estrategia) {
estrategies.put(nom, estrategia);
}
public CalculEnviament obtenir(String nom) {
CalculEnviament e = estrategies.get(nom);
if (e == null) throw new IllegalArgumentException("Politica desconeguda: " + nom);
return e;
}
}
// arrencada:
registre.registrar("distancia", new EnviamentPerDistancia(geo, base, perKm));
registre.registrar("plana", new EnviamentTarifaPlana(tarifa));
registre.registrar("gratis", new EnviamentGratisPromocio());
// us: la politica del restaurant es un String a la seva fitxa
CalculEnviament enviament = registre.obtenir(restaurant.getPoliticaEnviament());Strategy amb lambdes i Function
Amb un sol mètode a la interfície, Strategy és interfície funcional — i les estratègies sense dependències ni estat caben en lambdes:
@FunctionalInterface
public interface CalculEnviament {
BigDecimal calcular(Comanda comanda);
}
CalculEnviament plana = comanda -> new BigDecimal("35.00");
CalculEnviament gratis = comanda -> BigDecimal.ZERO;De fet, fa anys que uses Strategy amb lambdes: Comparator és una estratègia d'ordenació (llista.sort(comparing(Plat::getPreu))), els Predicate de filter(...) són estratègies de selecció, i Function/BiFunction serveixen d'interfície Strategy genèrica sense declarar res (Function<Comanda, BigDecimal>). El criteri, idèntic al de Command: lambda per a algorismes petits sense dependències; classe quan l'estratègia té col·laboradors injectats, estat o mereix tests propis. EnviamentPerDistancia (amb ServeiGeo i arrodoniments) és classe; la tarifa plana viu feliç en una lambda. Un matís a favor de la interfície pròpia davant de Function pelada: CalculEnviament diu què és a les firmes i permet evolucionar (un mètode descripcio() per al desglossament del tiquet) sense trencar res.
Strategy vs. State vs. Bridge: la taula
Els dos acaraments que la lliçó havia de saldar, junts — perquè el diagrama dels tres és gairebé idèntic (context/abstracció que delega en una interfície) i la diferència és d'intenció:
| Criteri | Strategy | State | Bridge |
|---|---|---|---|
| Què encapsula | Un algorisme complet | El comportament d'una etapa del cicle de vida | La implementació d'una abstracció (un "com" tècnic) |
| Qui canvia l'objecte | El client/configuració, des de fora | Els estats mateixos, en transitar | Ningú normalment: es fixa en construir |
| Les implementacions es coneixen entre si? | No: cada estratègia ignora les seves germanes | Sí: cada estat sap a quins es transita | No |
| Cardinalitat típica | Un context, un algorisme alhora | Un context, un estat alhora, canviant en seqüència | Dues jerarquies completes evolucionant a part |
| Pregunta que respon | Com faig aquest càlcul? | Què puc fer ara mateix? | Sobre quin mecanisme es recolza aquesta família? |
| A PideYa | CalculEnviament, EstrategiaAssignacio |
EstatComanda |
Notificacio × CanalEnviament |
La prova ràpida quan dubtis entre Strategy i State: pregunta't qui i per què canvia l'objecte delegat. El tria l'exterior segons preferència/configuració i les variants no es coneixen? Strategy. Canvia sol, com a conseqüència de les operacions, seguint un graf de transicions? State.
Quan usar-lo i quan no
Usa'l quan:
- Existeixen (o es preveuen de manera concreta) diverses maneres de fer el mateix i l'elecció varia per configuració, mercat, client o context.
- Un condicional sobre "el mode de càlcul" es repeteix o creix dins de classes que no haurien de conèixer aquests detalls.
- Vols testejar algorismes aïllats dels seus contexts, o amagar al context dades/dependències que només l'algorisme necessita.
Evita'l quan:
- Només hi ha un algorisme i les "variants futures" són especulació: interfície + una implementació + injecció per a res és sobreenginyeria de manual (lliçó 01-06). Extreu l'estratègia quan arribi la segona variant real.
- Les variants difereixen en un pas petit d'un flux comú, no en l'algorisme complet: allà encaixa millor el patró de la propera lliçó.
- La variació cap en un paràmetre (la tarifa plana de Mèxic vs. Colòmbia és un
BigDecimal, no una classe nova).
Relació amb altres patrons
- Template Method (lliçó següent): el mateix problema —variar parts d'un algorisme— resolt amb herència en comptes de composició; acarament complet allà.
- State i Bridge: la taula de dalt.
- Command: Command reïfica que es va demanar una cosa (amb undo, cua, auditoria); Strategy, com es fa una cosa. Acarament a la comparativa.
- Abstract Factory: fabrica l'estratègia coherent amb cada família/mercat.
- Decorator: el
SelectorEnviamentque embolcalla una altra estratègia afegeix una decisió sense canviar la interfície — mecànica de decorador sobre estratègies. - Flyweight: estratègies sense estat, compartides com a instàncies úniques.
Errors comuns
- El context que pregunta
instanceofa la seva estratègia ("si és la de distància, llavors també..."): trenca el contracte sencer; si el context necessita saber quina és, la interfície està mal dissenyada (falta un mètode que expressi aquesta variació). - Firmes anèmiques que van creixent:
calcular(BigDecimal total)es queda curta quan arriba l'estratègia per distància, i cada ampliació trenca totes les implementacions. Passa l'objecte de domini (Comanda) o un paràmetre-objecte des del principi. - Estratègies que muten el context o la comanda: una estratègia de càlcul amb efectes col·laterals (marcar la comanda, escriure en BD) és una mina; mantén-les pures sempre que el domini ho permeti.
- Compartir estratègies amb estat entre fils o contexts sense pensar-ho: el round-robin amb el seu comptador és compartit a propòsit; fes-lo thread-safe (el nostre
AtomicInteger) o no el comparteixis. - Un enum-switch disfressat:
switch (tipusEstrategia)dins del context per cridar mètodes diferents no és Strategy, és el que Strategy va venir a eliminar. El context no tria entre branques: rep l'objecte ja triat.
Exercicis
Exercici 1: estratègia d'hora vall
Implementa EnviamentHoraVall: fora de les franges punta (13–15 h i 20–22 h), aplica el 50% del càlcul d'una altra estratègia (la normal del mercat); en hora punta delega sense descompte. Pista: es construeix embolcallant una altra CalculEnviament. Quin patró estructural estàs reutilitzant?
Exercici 2: estratègia com a lambda... o no?
Per a cada política, decideix lambda o classe, i per què: (a) enviament gratis; (b) assignació per menor distància (usa ServeiGeo); (c) "el repartidor amb millor valoració mitjana, desempatant per menys lliuraments avui"; (d) ordenar els plats del tiquet alfabèticament.
Exercici 3: acarament State/Strategy en codi aliè
Un company modela els mètodes de pagament així: comanda.setComportamentPagament(new PagamentTargeta()), i dins de PagamentTargeta.processar() hi ha un comanda.setComportamentPagament(new PagamentCompletat()). És Strategy o State? Què delata la resposta, i què li recomanaries?
Solucions
Solució 1:
public class EnviamentHoraVall implements CalculEnviament {
private static final List<LocalTime[]> PUNTA = List.of(
new LocalTime[]{LocalTime.of(13, 0), LocalTime.of(15, 0)},
new LocalTime[]{LocalTime.of(20, 0), LocalTime.of(22, 0)});
private final CalculEnviament base;
private final Supplier<LocalTime> rellotge; // injectat per poder testejar hores
public EnviamentHoraVall(CalculEnviament base, Supplier<LocalTime> rellotge) {
this.base = base;
this.rellotge = rellotge;
}
@Override
public BigDecimal calcular(Comanda comanda) {
BigDecimal normal = base.calcular(comanda);
return esHoraPunta(rellotge.get())
? normal
: normal.multiply(new BigDecimal("0.5")).setScale(2, RoundingMode.HALF_UP);
}
private boolean esHoraPunta(LocalTime ara) {
return PUNTA.stream().anyMatch(f -> !ara.isBefore(f[0]) && ara.isBefore(f[1]));
}
}El patró reutilitzat és Decorator: mateixa interfície, embolcalla una CalculEnviament i en modifica el resultat — els patrons es componen entre famílies amb tota naturalitat. (I fixa't en el rellotge injectat: sense ell, els tests dependrien de l'hora a què els executis.)
Solució 2: (a) lambda (comanda -> BigDecimal.ZERO): sense estat ni dependències; (b) classe: dependència injectada (ServeiGeo) i mereix tests propis; (c) classe (o almenys un Comparator amb nom construït amb comparing(...).thenComparing(...)): la regla de negoci composta mereix nom i tests; (d) lambda/referència a mètode (comparing(LiniaComanda::getDescripcio)): és un Comparator trivial — que, recordem-ho, ja és Strategy de sèrie al JDK.
Solució 3: és State disfressat de Strategy: l'objecte delegat se substitueix a si mateix des de dins (PagamentTargeta transita a PagamentCompletat), que és exactament la firma de State — les "estratègies" es coneixen entre si i formen un graf de transicions. Ho delata la pregunta de la taula: no el tria l'exterior; canvia com a conseqüència de les operacions. Recomanació: rebatejar-lo i dissenyar-lo com a màquina d'estats explícita (interfície EstatPagament, transicions amb una porta única tipus transitarA, diagrama d'estats) — amb noms de Strategy, el lector buscarà intercanviabilitat externa on hi ha cicle de vida, i viceversa.
Conclusió
Strategy reïfica l'algorisme: CalculEnviament i EstrategiaAssignacio converteixen polítiques enterrades en condicionals en famílies de classes intercanviables — injectades per configuració, triades per context o servides des d'un registre estil RegistreNotificadors, amb lambdes per a les variants lleugeres i classes per a les que carreguen dependències. La FacanaCheckout i la CentralRepartiment queden com han de quedar: orquestrant, sense saber com es calcula res. I t'endús la taula que desarma les dues confusions clàssiques: davant de State (qui canvia l'objecte i per què?) i davant de Bridge (algorisme complet o fonament tècnic d'una abstracció?).
Queda una tercera frontera per traçar, l'anunciada a "quan no": i si les variants no són algorismes complets, sinó passos solts d'un flux que és sempre el mateix? Els informes de tancament diari de PideYa carreguen dades, agreguen, formaten i distribueixen — idèntic esquelet, passos diferents segons el format. Per a això el GoF té el seu patró d'herència per excel·lència, amb el seu famós principi de Hollywood: "no ens truquis; nosaltres et trucarem". Ens veiem a Template Method.
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
