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

  1. El problema a PideYa: algorismes enterrats en condicionals
  2. Intenció i estructura del patró
  3. Implementació Java completa: despeses d'enviament
  4. Segon cas: assignació de repartidor
  5. Seleccionar l'estratègia: config, context i registre
  6. Strategy amb lambdes i Function
  7. Strategy vs. State vs. Bridge: la taula
  8. Quan usar-lo i quan no
  9. Relació amb altres patrons
  10. Errors comuns
  11. 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 if en 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:

public interface CalculEnviament {
    BigDecimal calcular(Comanda comanda);
}

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 SelectorEnviament que 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 instanceof a 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

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