Màrqueting vol llançar promocions sense esperar el següent desplegament: "si el total supera els 30 € i és divendres, 10% de descompte"; "si és la primera comanda o el total supera els 50 €, enviament gratis". Cada regla nova avui és una classe Promocio nova escrita per un programador. La interfície Promocio del mòdul 1 ens va donar OCP —afegir sense modificar—, però màrqueting vol més: escriure les regles com a text i que el sistema les entengui. Això exigeix definir un minillenguatge: una gramàtica, arbres d'expressions i un intèrpret que les avaluï. Aquest és Interpreter — i aquesta lliçó inclou una dosi deliberada d'honestedat, perquè és el patró GoF que menys usaràs, i convé saber exactament per què.

Contingut

  1. El problema a PideYa: regles de promoció com a text
  2. La gramàtica del minillenguatge
  3. Intenció i estructura del patró
  4. Implementació Java completa
  5. I qui construeix l'arbre? El parser
  6. Quan usar-lo i quan no (honestedat inclosa)
  7. Relació amb altres patrons
  8. Errors comuns
  9. Exercicis i conclusió

El problema a PideYa: regles de promoció com a text

El que màrqueting vol escriure al panell d'administració:

total > 30 I dia == DIVENDRES
primeraComanda O total > 50
(total > 20 I dia == DILLUNS) O codiPostal == 28001

Cada línia és una condició d'aplicabilitat d'una promoció. El descompte en si (10%, enviament gratis) és una dada a part; el difícil és avaluar la condició contra cada comanda. Les forces:

  • Les regles combinen peces fixes (total, dia, primera comanda) amb connectors (I, O, comparacions, parèntesis): no és una llista tancada de casos, és un llenguatge petit però componible — hi ha infinites regles possibles.
  • Qui escriu les regles no programa; qui programa no vol desplegar per cada regla.
  • Les regles s'han d'avaluar contra dades de cada comanda en el moment del checkout.

Un if per regla no escala (torna el desplegament); un parser "a pèl" amb split i expressions regulars s'ensorra amb els parèntesis i la precedència. La solució amb estructura: tractar cada regla com una frase d'un llenguatge, representar-la com un arbre i donar a cada tipus de node la capacitat d'interpretar-se a si mateix.

La gramàtica del minillenguatge

Tot llenguatge, per petit que sigui, té una gramàtica. La nostra, en notació informal (cada línia és una regla de producció; el que es pot "expandir" més és no terminal, el que ja no, terminal):

expressio   ::= comparacio | expressio "I" expressio | expressio "O" expressio | "(" expressio ")"
comparacio  ::= variable operador literal
variable    ::= "total" | "dia" | "primeraComanda" | "codiPostal"
operador    ::= ">" | "<" | "=="
literal     ::= nombre | dia de la setmana | booleà

I la frase total > 30 I dia == DIVENDRES es converteix en aquest arbre de sintaxi abstracta (AST):

flowchart TD
    Y["I (no terminal)"] --> C1["&gt; (comparació)"]
    Y --> C2["== (comparació)"]
    C1 --> V1["total (terminal)"]
    C1 --> L1["30 (terminal)"]
    C2 --> V2["dia (terminal)"]
    C2 --> L2["DIVENDRES (terminal)"]

La idea central d'Interpreter: una classe per regla de la gramàtica. Els nodes fulla (variables, literals) són expressions terminals; els nodes amb fills (I, O, comparacions) són expressions no terminals que s'interpreten interpretant recursivament els seus fills. Un arbre de peces amb operació recursiva uniforme? Sí: estructuralment, Interpreter és un Composite l'operació del qual és interpretar.

Intenció i estructura del patró

Intenció (GoF): donat un llenguatge, definir una representació de la seva gramàtica juntament amb un intèrpret que usa aquesta representació per interpretar les frases del llenguatge.

classDiagram
    class ExpressioRegla {
        <<interface>>
        +interpretar(ctx: ContextComanda) boolean
    }
    class Comparacio {
        -variable: String
        -operador: String
        -literal: String
        +interpretar(ctx) boolean
    }
    class ExpressioI {
        -esquerra: ExpressioRegla
        -dreta: ExpressioRegla
        +interpretar(ctx) boolean
    }
    class ExpressioO {
        -esquerra: ExpressioRegla
        -dreta: ExpressioRegla
        +interpretar(ctx) boolean
    }
    class ContextComanda {
        +valorDe(variable: String) String
    }

    ExpressioRegla <|.. Comparacio
    ExpressioRegla <|.. ExpressioI
    ExpressioRegla <|.. ExpressioO
    ExpressioI o-- ExpressioRegla : fills
    ExpressioO o-- ExpressioRegla : fills
    ExpressioRegla ..> ContextComanda : llegeix
Rol GoF A PideYa
AbstractExpression ExpressioRegla
TerminalExpression (fulles) Comparacio (encapsula variable-operador-literal)
NonterminalExpression (nodes amb fills) ExpressioI, ExpressioO
Context (dades externes que necessita l'avaluació) ContextComanda
Client (construeix/obté l'AST i l'interpreta) el motor de promocions

Implementació Java completa

El context: la finestra per la qual les expressions veuen la comanda. Aïlla la gramàtica del model de domini — si demà Comanda canvia, només canvia el context:

public class ContextComanda {

    private final Map<String, String> valors;

    public ContextComanda(Comanda comanda, Client client) {
        this.valors = Map.of(
            "total",          comanda.getTotal().toPlainString(),
            "dia",            LocalDate.now().getDayOfWeek().name(), // "FRIDAY"...
            "primeraComanda", String.valueOf(client.getNombreComandes() == 0),
            "codiPostal",     comanda.getAdrecaLliurament().getCodiPostal()
        );
    }

    public String valorDe(String variable) {
        String v = valors.get(variable);
        if (v == null) {
            throw new ReglaInvalidaException("Variable desconeguda: " + variable);
        }
        return v;
    }
}

La interfície i les expressions no terminals. Mira que petites: cadascuna implementa una regla de la gramàtica delegant en els seus fills:

public interface ExpressioRegla {
    boolean interpretar(ContextComanda ctx);
}

public class ExpressioI implements ExpressioRegla {
    private final ExpressioRegla esquerra, dreta;

    public ExpressioI(ExpressioRegla esquerra, ExpressioRegla dreta) {
        this.esquerra = esquerra;
        this.dreta = dreta;
    }

    @Override
    public boolean interpretar(ContextComanda ctx) {
        return esquerra.interpretar(ctx) && dreta.interpretar(ctx);
    }
}

public class ExpressioO implements ExpressioRegla {
    private final ExpressioRegla esquerra, dreta;

    public ExpressioO(ExpressioRegla esquerra, ExpressioRegla dreta) {
        this.esquerra = esquerra;
        this.dreta = dreta;
    }

    @Override
    public boolean interpretar(ContextComanda ctx) {
        return esquerra.interpretar(ctx) || dreta.interpretar(ctx);
    }
}

L'expressió terminal fa la feina bruta de comparar contra el context:

public class Comparacio implements ExpressioRegla {

    private final String variable, operador, literal;

    public Comparacio(String variable, String operador, String literal) {
        this.variable = variable;
        this.operador = operador;
        this.literal = literal;
    }

    @Override
    public boolean interpretar(ContextComanda ctx) {
        String valor = ctx.valorDe(variable);
        return switch (operador) {
            case "==" -> valor.equalsIgnoreCase(literal);
            case ">"  -> new BigDecimal(valor).compareTo(new BigDecimal(literal)) > 0;
            case "<"  -> new BigDecimal(valor).compareTo(new BigDecimal(literal)) < 0;
            default   -> throw new ReglaInvalidaException("Operador desconegut: " + operador);
        };
    }
}

Muntar i avaluar la regla total > 30 I dia == DIVENDRES a mà:

ExpressioRegla regla = new ExpressioI(
    new Comparacio("total", ">", "30"),
    new Comparacio("dia", "==", "FRIDAY")
);

boolean aplica = regla.interpretar(new ContextComanda(comanda, client));

I així, la regla encaixa netament en la interfície Promocio de sempre: una PromocioAmbRegla que guarda la seva ExpressioRegla i el seu descompte, i a aplicaA(comanda) interpreta. El motor de promocions no sap que a dins hi ha un llenguatge: només veu Promocio. OCP intacte — i ara, sense desplegar.

I qui construeix l'arbre? El parser

Aquí hi ha la lletra petita del patró: Interpreter no diu com passar del text a l'arbre. El GoF ho declara explícitament fora d'abast. Convertir "(total > 20 I dia == DILLUNS) O codiPostal == 28001" en objectes exigeix un parser: trossejar el text (anàlisi lèxica), respectar precedències i parèntesis (anàlisi sintàctica) i construir l'AST. Per a la nostra gramàtica diminuta, un parser recursiu descendent són ~60 línies assumibles (l'exercici 2 et fa escriure'n una versió mínima). Però és la part que pitjor envelleix: cada extensió de la gramàtica toca el parser i afegeix classes.

Per a gramàtiques de debò, no reinventis: generadors de parsers com ANTLR o JavaCC generen l'analitzador a partir de la gramàtica declarada, i llenguatges embeguts ja fets (SpEL de Spring, MVEL, JEXL, fins i tot JavaScript embegut amb GraalVM) et donen variables, operadors, funcions i seguretat ja resolts. La frontera pràctica: si la teva gramàtica cap en cinc regles de producció i creixerà poc, l'Interpreter casolà és raonable; si no, estàs construint un compilador per accident.

Quan usar-lo i quan no (honestedat inclosa)

Usa'l quan:

  • Existeix un llenguatge petit, estable i componible que usuaris o configuració necessiten escriure: regles de negoci, filtres de cerca, expressions de permisos.
  • La gramàtica és simple (poques regles de producció) i l'eficiència no és crítica.
  • Les frases es representen bé com a arbres i s'avaluen recursivament.

Evita'l quan:

  • La gramàtica és o serà complexa: la jerarquia de classes creix amb cada regla de producció, i el manteniment (classes + parser) es dispara. Usa ANTLR/JavaCC o un llenguatge embegut existent.
  • El que necessites és una llista tancada d'opcions, no un llenguatge componible: això és Strategy o simple configuració.
  • El rendiment importa molt: interpretar arbres d'objectes és lent comparat amb codi compilat.

La dosi d'honestedat promesa. Interpreter és, amb diferència, el patró GoF que menys aplicaràs a mà, per tres raons acumulades: (1) pocs dominis justifiquen inventar un llenguatge propi; (2) quan ho justifiquen, les eines modernes (parser generators, motors de regles, llenguatges embeguts) fan la feina millor que una jerarquia casolana; (3) el seu nínxol original dels 90 —minillenguatges ad hoc— avui el cobreixen JSON/YAML per a configuració i expressions estàndard per a regles. Llavors, per què estudiar-lo? Perquè el seu model mental —gramàtica → AST → avaluació recursiva— és exactament com funcionen les expressions regulars que uses cada dia, el parseig d'SQL, els motors de plantilles i els compiladors mateixos. Entendre Interpreter és entendre què hi ha dins d'aquestes caixes negres; escriure'l des de zero, gairebé mai no és la decisió correcta en producció.

Relació amb altres patrons

  • Composite: l'AST és un composite; Interpreter hi afegeix l'operació d'avaluació recursiva sobre aquesta estructura.
  • Visitor: si a més d'avaluar vols imprimir, optimitzar o validar l'arbre, en comptes d'engreixar cada classe d'expressió amb més mètodes, un visitant afegeix operacions sense tocar-les — combinació clàssica sobre ASTs.
  • Iterator: recórrer l'arbre d'expressions sense exposar-ne l'estructura.
  • Flyweight: els símbols terminals repetits (la variable total en mil regles) es poden compartir.
  • Strategy: l'alternativa quan no hi ha llenguatge, només variants.

Errors comuns

  • Inventar un llenguatge quan bastava una llista d'opcions: si màrqueting només necessita triar entre cinc condicions predefinides, un desplegable i un Map ho resolen; el llenguatge es justifica quan la combinatòria és el requisit.
  • Deixar créixer la gramàtica "una miqueta més" cada mes: funcions, aritmètica, cadenes... A la tercera ampliació tens un llenguatge de programació sense voler. Posa frontera per escrit des del dia u.
  • Avaluar sense validar: regles escrites per humans arriben mal escrites. El parser ha de donar errors comprensibles ("parèntesi sense tancar a la posició 24"), no NullPointerException al checkout.
  • Oblidar la seguretat: si algun dia embeus un llenguatge real (JS, expressions amb reflexió com SpEL), estàs executant codi d'usuaris — sandbox i llista blanca de variables obligatòries.
  • Acoblar les expressions al domini: si Comparacio rep Comanda directament, cada canvi del model trenca la gramàtica. Per a això hi ha el ContextComanda.

Exercicis

Exercici 1: negació

Màrqueting demana regles com NO primeraComanda I total > 15. Afegeix a la gramàtica la producció expressio ::= "NO" expressio i implementa la classe corresponent. És terminal o no terminal? Quantes classes existents has tocat?

Exercici 2: parser mínim

Escriu ParserRegles.parsejar(String) per al subconjunt sense parèntesis ni O: comparacions unides per I (p. ex. total > 30 I dia == FRIDAY I primeraComanda == true). Pista: separa per " I ", parseja cada comparació per espais, i plega la llista amb ExpressioI.

Exercici 3: criteri

Per a cada cas, decideix: Interpreter casolà, eina existent, o ni tan sols llenguatge? (a) els repartidors filtren comandes per "zona == CENTRE I total > 20"; (b) finances vol fórmules tipus full de càlcul amb funcions, dates i aritmètica per calcular comissions; (c) operacions vol activar/desactivar el control antifrau per país.

Solucions

Solució 1: no terminal (té una subexpressió filla). Classes tocades: zero — només se n'afegeix una (OCP a la gramàtica, herència del disseny Composite):

public class ExpressioNo implements ExpressioRegla {
    private final ExpressioRegla interior;

    public ExpressioNo(ExpressioRegla interior) { this.interior = interior; }

    @Override
    public boolean interpretar(ContextComanda ctx) {
        return !interior.interpretar(ctx);
    }
}

(El parser sí que s'hauria de tocar — exactament el cost dual que assenyala la lliçó.)

Solució 2:

public class ParserRegles {

    public static ExpressioRegla parsejar(String text) {
        String[] parts = text.split(" I ");
        ExpressioRegla resultat = parsejarComparacio(parts[0]);
        for (int i = 1; i < parts.length; i++) {
            resultat = new ExpressioI(resultat, parsejarComparacio(parts[i]));
        }
        return resultat;
    }

    private static ExpressioRegla parsejarComparacio(String text) {
        String[] t = text.trim().split("\\s+");   // ["total", ">", "30"]
        if (t.length != 3) {
            throw new ReglaInvalidaException("Comparacio mal formada: '" + text + "'");
        }
        return new Comparacio(t[0], t[1], t[2]);
    }
}

Fixa't com fins i tot aquesta joguina ja valida i dona missatges útils. Afegir O amb menor precedència que I i parèntesis exigiria un recursiu descendent amb un nivell per precedència — el salt de complexitat que t'empeny cap a ANTLR.

Solució 3: (a) Interpreter casolà raonable: gramàtica diminuta, componible, estable — és el nostre cas de la lliçó amb altres variables; (b) eina existent (motor d'expressions tipus SpEL/JEXL o fins i tot una llibreria de fulls de càlcul): funcions + aritmètica + dates és una gramàtica gran, la mantindries per sempre; (c) ni llenguatge: és un booleà per país — configuració pura a ConfiguracioPideYa.

Conclusió

Interpreter tanca el trio de patrons que reïfiquen peticions: la cadena les encaminava, Command les congelava, i Interpreter va més enllà i reïfica frases senceres d'un llenguatge: una classe per regla de la gramàtica, arbres d'expressions que s'avaluen recursivament contra un context, i màrqueting escrivint promocions sense desplegar. També t'endús la seva lletra petita: el parser no ve inclòs, la jerarquia creix amb la gramàtica, i en producció gairebé sempre guanyen ANTLR o un llenguatge embegut — però el model gramàtica → AST → avaluació és una de les idees més rendibles que t'endús del catàleg, perquè és dins de cada regex, cada SQL i cada compilador que uses.

El nostre AST era un arbre que sabíem recórrer perquè l'havíem construït nosaltres. Però, i les estructures que no volem que ningú conegui per dins? La carta Composite de La Bella Napoli té seccions dins de seccions, i avui cada pantalla que la recorre reimplementa la recursió pel seu compte. Recórrer sense exposar: aquest és el patró següent, el més humil i el més omnipresent de tots — l'uses cada cop que escrius un for-each. Ens veiem a Iterator.

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