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
- El problema a PideYa: regles de promoció com a text
- La gramàtica del minillenguatge
- Intenció i estructura del patró
- Implementació Java completa
- I qui construeix l'arbre? El parser
- Quan usar-lo i quan no (honestedat inclosa)
- Relació amb altres patrons
- Errors comuns
- 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 == 28001Cada 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["> (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
totalen 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
Mapho 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
NullPointerExceptional 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
ComparaciorepComandadirectament, cada canvi del model trenca la gramàtica. Per a això hi ha elContextComanda.
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
- 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
