Els patrons de disseny no són receptes arbitràries: cadascun existeix per fer complir un o diversos principis de disseny. Els principis són el perquè; els patrons, el com. Si estudies patrons sense principis, els aplicaràs mecànicament i on no toquen; si domines els principis, entendràs cada patró a la primera i sabràs jutjar quan val la pena. En aquesta lliçó estudiarem els cinc principis SOLID amb exemples de violació i correcció sobre PideYa, i els complementarem amb altres fonaments imprescindibles: DRY, KISS, YAGNI i les dues màximes del Gang of Four. És, probablement, la lliçó més important del mòdul.

Contingut

  1. Què és un principi de disseny i per què precedeix els patrons
  2. S — Principi de Responsabilitat Única (SRP)
  3. O — Principi Obert/Tancat (OCP)
  4. L — Principi de Substitució de Liskov (LSP)
  5. I — Principi de Segregació d'Interfícies (ISP)
  6. D — Principi d'Inversió de Dependències (DIP)
  7. Altres fonaments: DRY, KISS i YAGNI
  8. Les dues màximes del GoF
  9. Dels principis als patrons

Què és un principi de disseny i per què precedeix els patrons

Un principi de disseny és una directriu general sobre com organitzar el codi perquè sigui fàcil d'entendre, canviar i provar. A diferència d'un patró, un principi no proposa una estructura concreta de classes: proposa un criteri de qualitat. Els cinc més citats formen l'acrònim SOLID, popularitzat per Robert C. Martin ("Uncle Bob"):

Lletra Principi Idea en una frase
S Single Responsibility Una classe ha de tenir una única raó per canviar
O Open/Closed Obert a extensió, tancat a modificació
L Liskov Substitution Les subclasses s'han de poder usar on s'espera la superclasse
I Interface Segregation Millor moltes interfícies petites que una de gran
D Dependency Inversion Depèn d'abstraccions, no d'implementacions concretes

Per a cada principi seguirem el mateix mètode: enunciat, violació a PideYa (codi Java real i el seu problema) i correcció.

S — Principi de Responsabilitat Única (SRP)

Enunciat: una classe ha de tenir una única responsabilitat; dit amb més precisió, una única raó per canviar. Si dos requisits diferents (de dos "clients" diferents: negoci, comptabilitat, màrqueting...) obliguen a tocar la mateixa classe, aquesta classe fa massa coses.

Violació a PideYa

public class Comanda {
    private List<LiniaComanda> linies;
    private Client client;

    public double calcularTotal() {
        return linies.stream()
                     .mapToDouble(l -> l.getPreu() * l.getQuantitat())
                     .sum();
    }

    // Responsabilitat d'una comanda? Generar la seva propia factura en PDF...
    public byte[] generarFacturaPdf() {
        // ...logica de maquetacio, fonts, logotip...
        return new byte[0];
    }

    // ...i tambe desar-se a si mateixa a la base de dades?
    public void desarABaseDeDades() {
        // ...SQL, connexions, transaccions...
    }
}

Aquesta classe té tres raons per canviar: canvia si canvia la lògica de negoci (nous descomptes), si canvia el format de la factura (nou logotip, requisit legal), i si canvia la persistència (migrar de MySQL a PostgreSQL). Tres equips diferents tocant el mateix fitxer: conflictes, risc i tests impossibles d'aïllar.

Correcció

public class Comanda {                 // Nomes logica de negoci de la comanda
    private List<LiniaComanda> linies;
    public double calcularTotal() { /* ... */ return 0; }
}

public class GeneradorFactures {      // Nomes presentacio de factures
    public byte[] generarPdf(Comanda comanda) { /* ... */ return new byte[0]; }
}

public class RepositoriComandes {     // Nomes persistencia
    public void desar(Comanda comanda) { /* ... */ }
}

Cada classe té ara una responsabilitat i un motiu de canvi. Fixa't que l'SRP no diu "classes petites": diu cohesionades. Una classe de 300 línies amb una sola responsabilitat compleix l'SRP; una de 30 línies amb dues, no.

O — Principi Obert/Tancat (OCP)

Enunciat: les entitats de programari han d'estar obertes a l'extensió (poder afegir comportament nou) però tancades a la modificació (sense editar el codi existent que ja funciona). L'eina per aconseguir-ho és l'abstracció: punts d'extensió mitjançant interfícies.

Violació a PideYa

PideYa aplica descomptes segons el tipus de promoció:

public class CalculadoraDescomptes {
    public double calcular(Comanda comanda, String tipusPromocio) {
        if (tipusPromocio.equals("PRIMERA_COMPRA")) {
            return comanda.calcularTotal() * 0.10;
        } else if (tipusPromocio.equals("ENVIAMENT_GRATIS")) {
            return comanda.getDespesesEnviament();
        } else if (tipusPromocio.equals("CUPO_5_EUROS")) {
            return 5.0;
        }
        return 0;
    }
}

Cada promoció nova de màrqueting obliga a modificar aquesta classe: un altre else if, una altra oportunitat de trencar les promocions que ja funcionaven, un altre desplegament amb risc. La cadena d'if/else if sobre un "tipus" és el símptoma clàssic de violació de l'OCP.

Correcció

public interface Promocio {
    double calcularDescompte(Comanda comanda);
}

public class PromocioPrimeraCompra implements Promocio {
    public double calcularDescompte(Comanda comanda) {
        return comanda.calcularTotal() * 0.10;
    }
}

public class PromocioEnviamentGratis implements Promocio {
    public double calcularDescompte(Comanda comanda) {
        return comanda.getDespesesEnviament();
    }
}

public class CalculadoraDescomptes {
    public double calcular(Comanda comanda, Promocio promocio) {
        return promocio.calcularDescompte(comanda);  // Tancada: no canvia mai
    }
}

Afegir la promoció "2x1 en pizzes" és ara afegir una classe nova, sense tocar ni la calculadora ni les promocions existents. El codi provat es manté intacte. (Si això et fa olor de patró amb nom propi, tens bon nas: diversos patrons del mòdul 4 són exactament aquesta jugada; no avancem esdeveniments.)

L — Principi de Substitució de Liskov (LSP)

Enunciat (Barbara Liskov, 1987): si S és subtipus de T, els objectes de S s'han de poder usar en qualsevol lloc on s'esperi un T sense que el programa deixi de ser correcte. A la pràctica: una subclasse no pot endurir les condicions d'entrada, afeblir les garanties de sortida, ni llançar sorpreses que la superclasse no anunciava.

Violació a PideYa

PideYa modela els mètodes de pagament, i a algú li va semblar natural que el pagament contra reemborsament "sigui un" mètode de pagament:

public class MetodePagament {
    /** Cobra la quantia i retorna un identificador de transaccio. */
    public String cobrar(double quantia) {
        // ...cobrament online...
        return "TX-123";
    }
}

public class PagamentContraReemborsament extends MetodePagament {
    @Override
    public String cobrar(double quantia) {
        // No es pot cobrar online: es paga al repartidor a la porta
        throw new UnsupportedOperationException("No aplicable");
    }
}

El codi client confia en el contracte de MetodePagament:

public void confirmarComanda(Comanda comanda, MetodePagament pagament) {
    String tx = pagament.cobrar(comanda.calcularTotal()); // Explota amb contra reemborsament!
    comanda.marcarComPagada(tx);
}

PagamentContraReemborsament és un MetodePagament segons el compilador, però no segons el contracte: substituir-lo trenca el programa. Herència sintàcticament vàlida, semànticament fraudulenta.

Correcció

La solució és redissenyar l'abstracció perquè el contracte sigui complible per tots els subtipus:

public interface MetodePagament {
    /** Retorna el resultat de l'intent: cobrat ara o pendent al lliurament. */
    ResultatPagament processar(double quantia);
}

public class PagamentTargeta implements MetodePagament {
    public ResultatPagament processar(double quantia) {
        return ResultatPagament.cobrat("TX-123");
    }
}

public class PagamentContraReemborsament implements MetodePagament {
    public ResultatPagament processar(double quantia) {
        return ResultatPagament.pendentEnLliurament(quantia);
    }
}

Ara tots els implementadors compleixen el mateix contracte ("processar retorna un resultat, mai no llança per disseny") i el client pot tractar el resultat uniformement. Regla pràctica: si en heretar necessites anul·lar un mètode amb una excepció o deixar-lo buit, la jerarquia està mal plantejada.

I — Principi de Segregació d'Interfícies (ISP)

Enunciat: cap client no ha de veure's obligat a dependre de mètodes que no fa servir. Millor diverses interfícies petites i específiques que una interfície "grassa" que ho barreja tot.

Violació a PideYa

public interface Treballador {
    void prepararPlat(Plat plat);
    void lliurarComanda(Comanda comanda);
    void atendreTrucada(Trucada trucada);
}

public class Repartidor implements Treballador {
    public void prepararPlat(Plat plat) {
        throw new UnsupportedOperationException(); // Un repartidor no cuina
    }
    public void lliurarComanda(Comanda comanda) { /* ... */ }
    public void atendreTrucada(Trucada trucada) {
        throw new UnsupportedOperationException(); // ...ni aten el telefon
    }
}

Repartidor està obligat a "implementar" mètodes que no li corresponen (i fixa't-hi: la interfície grassa l'ha forçat a violar també l'LSP). A més, si canvia la signatura de prepararPlat, cal recompilar i revisar tots els que implementen Treballador, cuinin o no.

Correcció

public interface Cuiner {
    void prepararPlat(Plat plat);
}

public interface Reparteix {
    void lliurarComanda(Comanda comanda);
}

public interface AtenTelefon {
    void atendreTrucada(Trucada trucada);
}

public class Repartidor implements Reparteix {
    public void lliurarComanda(Comanda comanda) { /* ... */ }
}

// Un empleat polivalent d'un restaurant petit pot implementar-ne diverses:
public class EmpleatPolivalent implements Cuiner, AtenTelefon {
    public void prepararPlat(Plat plat) { /* ... */ }
    public void atendreTrucada(Trucada trucada) { /* ... */ }
}

Cada client depèn només del que necessita, i els rols es componen lliurement.

D — Principi d'Inversió de Dependències (DIP)

Enunciat: (1) els mòduls d'alt nivell (lògica de negoci) no han de dependre de mòduls de baix nivell (detalls tècnics): tots dos han de dependre d'abstraccions; (2) les abstraccions no han de dependre dels detalls, sinó els detalls de les abstraccions.

Violació a PideYa

public class ServeiComandes {
    private ServeiSmsTwilio sms = new ServeiSmsTwilio(); // Detall concret

    public void confirmar(Comanda comanda) {
        // ...logica de negoci...
        sms.enviarSms(comanda.getClient().getTelefon(), "Comanda confirmada!");
    }
}

La lògica de negoci (alt nivell) depèn d'un proveïdor concret d'SMS (baix nivell), i a més el crea amb new. Conseqüències: impossible canviar de proveïdor sense tocar el negoci, i impossible testejar ServeiComandes sense enviar SMS reals.

Correcció

public interface Notificador {                       // Abstraccio, definida
    void notificar(Client client, String missatge);  // per l'ALT nivell
}

public class NotificadorSmsTwilio implements Notificador {  // Detall
    public void notificar(Client client, String missatge) {
        // ...API de Twilio...
    }
}

public class ServeiComandes {
    private final Notificador notificador;

    public ServeiComandes(Notificador notificador) {  // Injeccio per constructor
        this.notificador = notificador;
    }

    public void confirmar(Comanda comanda) {
        // ...logica de negoci...
        notificador.notificar(comanda.getClient(), "Comanda confirmada!");
    }
}

La "inversió" del nom rau en la direcció de la dependència: abans, negoci → Twilio; ara, negoci → Notificador ← Twilio. El detall depèn de l'abstracció del negoci, i no al revés. Als tests n'hi ha prou de passar un Notificador fals. (La injecció de dependències que fan frameworks com Spring és l'automatització industrial d'aquest principi.)

flowchart LR
    subgraph Abans
        A[ServeiComandes] --> B[ServeiSmsTwilio]
    end
    subgraph Despres["Després"]
        C[ServeiComandes] --> I[«interface» Notificador]
        D[NotificadorSmsTwilio] -.->|implementa| I
    end

Altres fonaments: DRY, KISS i YAGNI

Tres principis més, menys formals però igual de citats:

  • DRY (Don't Repeat Yourself): cada peça de coneixement ha de tenir una representació única al sistema. Si la regla "l'enviament és gratuït a partir de 20 €" està copiada a la cistella, al checkout i a l'email de confirmació de PideYa, el dia que canviï a 25 € algú oblidarà un dels tres llocs. Compte: DRY parla de coneixement, no de línies semblants; dos fragments accidentalment iguals que evolucionaran per separat no s'han d'unificar.
  • KISS (Keep It Simple, Stupid): entre dos dissenys que resolen el problema, guanya el més simple. La complexitat només es justifica quan compra alguna cosa (flexibilitat que es farà servir, rendiment necessari). Aquest principi és el gran contrapès dels patrons: cada patró afegeix indirecció, i KISS t'obliga a preguntar-te si la necessites.
  • YAGNI (You Aren't Gonna Need It): no construeixis avui la flexibilitat que "potser" necessitaràs demà. Si PideYa només cobra amb targeta i no hi ha plans de res més, crear la jerarquia completa de MetodePagament "per si de cas" és cost sense benefici. YAGNI no prohibeix dissenyar bé: prohibeix especular. El senyal per generalitzar és un requisit real, no una intuïció.
Principi Protegeix contra... Tensió amb els patrons
DRY Duplicació de coneixement Els patrons ajuden a centralitzar variacions
KISS Complexitat innecessària Tot patró ha de justificar la seva indirecció
YAGNI Flexibilitat especulativa No apliquis un patró per a un futur imaginari

Les dues màximes del GoF

El llibre del Gang of Four condensa la seva filosofia en dues màximes que reapareixeran a tots els mòduls:

«Programa contra una interfície, no contra una implementació»

Declara les teves variables, paràmetres i retorns amb el tipus abstracte (interfície o classe abstracta), no amb el concret. Ja ho has vist en acció a l'OCP i el DIP:

// Malament: acoblat a la implementacio
ArrayList<Plat> carta = new ArrayList<>();
NotificadorSmsTwilio notificador = new NotificadorSmsTwilio();

// Be: acoblat nomes al contracte
List<Plat> carta = new ArrayList<>();
Notificador notificador = obtenirNotificador();

L'únic punt que coneix la classe concreta és el de creació (new). Reduir i centralitzar aquests punts és, exactament, la missió dels patrons creacionals del mòdul 2.

«Afavoreix la composició d'objectes per sobre de l'herència de classes»

L'herència és temptadora per reutilitzar codi, però acobla el fill amb les entranyes del pare, es fixa en temps de compilació i no es pot combinar (en Java només s'hereta d'una classe). La composició —tenir una referència a un altre objecte i delegar-hi— és flexible, combinable i canviable en temps d'execució.

Exemple a PideYa: per modelar repartidors en moto o bicicleta, heretar RepartidorEnMoto i RepartidorEnBici de Repartidor explota tan bon punt apareix una altra dimensió (torn de dia/nit → RepartidorEnMotoDeNit?). Amb composició:

public class Repartidor {
    private Vehicle vehicle;   // Composicio: "te un" vehicle

    public void assignarVehicle(Vehicle vehicle) {  // Canviable en calent
        this.vehicle = vehicle;
    }

    public int tempsEstimat(double km) {
        return vehicle.calcularMinuts(km);          // Delegacio
    }
}

Un repartidor pot canviar de moto a bici a mig torn sense canviar de classe. L'herència queda reservada per a veritables relacions "és-un" amb contracte respectat (LSP). La majoria de patrons estructurals i de comportament que veuràs són, en el fons, maneres enginyoses d'usar composició on un principiant usaria herència.

Dels principis als patrons

Tanquem amb la idea central de la lliçó: els patrons són aplicacions concretes, empaquetades i amb nom, d'aquests principis. Quan als propers mòduls estudiïs un patró, pregunta't sempre quins principis està servint; com a anticipació general (sense entrar encara en cap patró):

  • Els patrons creacionals (mòdul 2) existeixen sobretot per servir el DIP i "programa contra interfícies": aïllen els new perquè la resta del codi depengui només d'abstraccions.
  • Els patrons estructurals (mòdul 3) són exercicis de composició per sobre d'herència: embolcallar, adaptar i compondre objectes.
  • Els patrons de comportament (mòdul 4) exploten l'OCP i l'SRP: extreuen comportaments variables a jerarquies pròpies per estendre sense modificar.

I a la inversa: quan detectis una violació d'un principi (una cadena d'if/else if per tipus, un new enmig del negoci, una subclasse que llança UnsupportedOperationException), tindràs el senyal que probablement existeix un patró pensat per a aquesta situació.

Errors Comuns i Consells

  • Aplicar SRP com a "classes diminutes". El criteri és raons per canviar, no línies de codi. Fragmentar en excés crea un altre problema: lògica polvoritzada en vint classes anèmiques.
  • Perseguir l'OCP de manera preventiva. No posis una interfície davant de tot "per si de cas": això viola YAGNI. Tanca contra modificació els eixos on el canvi ja ha passat o està anunciat (les promocions de PideYa canvien cada mes: aquí sí).
  • Verificar l'LSP només amb el compilador. Que compili no vol dir que substitueixi. Pregunta't: pot tot el codi que fa servir la superclasse rebre aquesta subclasse sense sorpreses? Les excepcions inesperades i els mètodes buits són el senyal d'alarma.
  • Confondre DIP amb "usar interfícies a tot arreu". La inversió rau en qui defineix l'abstracció (l'alt nivell) i en cap a on apunten les dependències, no en el nombre d'interfícies.
  • Tractar DRY, KISS i YAGNI com a absoluts. Són forces a equilibrar: DRY empeny a abstreure, KISS i YAGNI a no abstreure de més. El bon disseny viu en la tensió, no a l'extrem.
  • Consell: a la teva propera revisió de codi, busca només dos símptomes: cadenes d'if/else if sobre un "tipus" i new de classes concretes dins de lògica de negoci. Són les dues violacions més freqüents i les que més patrons motiven.

Exercicis

Exercici 1: diagnosticar violacions

Aquesta classe de PideYa viola diversos principis SOLID. Identifica'n almenys tres, indicant quins i per què:

public class GestorRestaurant {
    private ConnexioMySql connexio = new ConnexioMySql();

    public void donarAltaPlat(String nom, double preu, String tipus) {
        if (tipus.equals("PIZZA")) {
            // validacions especifiques de pizzes
        } else if (tipus.equals("SUSHI")) {
            // validacions especifiques de sushi
        }
        connexio.executar("INSERT INTO plats ...");
        enviarEmailAlPropietari(nom);
    }

    private void enviarEmailAlPropietari(String nomPlat) {
        // SMTP, plantilles HTML, reintents...
    }
}

Exercici 2: refactoritzar cap a OCP + DIP

PideYa calcula les despeses d'enviament segons la zona amb aquest codi. Refactoritza'l perquè afegir una zona nova no requereixi modificar la classe, i perquè la classe de negoci no creï les seves dependències:

public class CalculadoraEnviament {
    public double calcular(Comanda comanda) {
        String zona = comanda.getAdreca().getZona();
        if (zona.equals("CENTRE")) return 1.50;
        else if (zona.equals("PERIFERIA")) return 3.00;
        else return 5.00;
    }
}

Exercici 3: herència contra composició

Un company proposa modelar els plats amb descompte com a subclasses: PlatAmbDescompte10 extends Plat, PlatAmbDescompte20 extends Plat. Argumenta per què és mala idea (cita almenys dos principis o màximes d'aquesta lliçó) i esbossa en pseudocodi una alternativa basada en composició.

Solucions

Solució 1:

  • SRP: la classe barreja validació de negoci, persistència (SQL) i enviament d'emails: tres raons per canviar.
  • OCP: la cadena if/else if per tipus de plat obliga a modificar el mètode amb cada tipus nou.
  • DIP: el negoci depèn de ConnexioMySql concreta i la crea amb new; hauria de dependre d'una abstracció de persistència injectada. (També es pot argumentar que l'email incrustat viola de nou SRP/DIP: detall tècnic dins de l'alt nivell.)

Solució 2 (una solució possible):

public interface TarifaZona {
    boolean aplicaA(Adreca adreca);
    double cost();
}

public class TarifaCentre implements TarifaZona {
    public boolean aplicaA(Adreca a) { return a.getZona().equals("CENTRE"); }
    public double cost() { return 1.50; }
}
// TarifaPeriferia, TarifaEstandard... analogues

public class CalculadoraEnviament {
    private final List<TarifaZona> tarifes;
    private final double costPerDefecte;

    public CalculadoraEnviament(List<TarifaZona> tarifes, double costPerDefecte) {
        this.tarifes = tarifes;               // Injectades: DIP
        this.costPerDefecte = costPerDefecte;
    }

    public double calcular(Comanda comanda) {
        return tarifes.stream()
                .filter(t -> t.aplicaA(comanda.getAdreca()))
                .findFirst()
                .map(TarifaZona::cost)
                .orElse(costPerDefecte);      // OCP: zones noves = classes noves
    }
}

Afegir la zona "URBANITZACIONS" és crear una classe i incloure-la a la llista injectada: la calculadora no es toca.

Solució 3: arguments: (1) explosió combinatòria i rigidesa de l'herència: cada percentatge nou és una subclasse, i un plat no pot guanyar o perdre el descompte en temps d'execució (un descompte és temporal per naturalesa), cosa que xoca amb "afavoreix la composició per sobre de l'herència"; (2) OCP/DRY: la lògica "aplicar un percentatge" queda duplicada a cada subclasse i afegir el 15% exigeix una classe nova idèntica a les altres; a més la relació real no és "és-un" (un plat amb descompte no és un tipus diferent de plat), senyal d'herència mal usada (l'esperit de l'LSP). Alternativa en pseudocodi:

classe Plat:
    preuBase
    descompte: Descompte        // composicio, pot ser "sense descompte"
    preuFinal() = descompte.aplicarA(preuBase)

interficie Descompte:
    aplicarA(preu)

classe DescomptePercentual implementa Descompte(percentatge)
classe SenseDescompte implementa Descompte   // retorna el preu tal qual

El descompte s'assigna, canvia o retira en temps d'execució sense tocar la classe Plat.

Conclusió

Ja tens el sistema de valors del disseny orientat a objectes: SOLID (una responsabilitat per classe, estendre sense modificar, subtipus que substitueixen de debò, interfícies a mida del client i dependències que apunten a abstraccions), temperat per DRY, KISS i YAGNI, i coronat per les dues màximes del GoF: programar contra interfícies i afavorir la composició. I, sobretot, la clau de volta del curs: cada patró que estudiaràs és un o diversos d'aquests principis convertits en estructura concreta; els principis et diran a més quan un patró sobra.

Per estudiar aquestes estructures necessitem poder dibuixar-les i llegir-les: els patrons es comuniquen amb diagrames de classes i de seqüència. A la propera lliçó aprendràs just l'UML imprescindible per a aquest curs: UML Essencial per Entendre Patrons.

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