SOLID és l'acrònim de cinc principis de disseny orientat a objectes popularitzats per Robert C. Martin. Encara que solen presentar-se com a regles de programació, el seu veritable valor és arquitectònic: quan s'apliquen amb criteri, produeixen sistemes els components dels quals són independents, substituïbles i resistents al canvi. Cada principi ataca una forma concreta de rigidesa o fragilitat. En aquesta lliçó estudiarem els cinc, tot veient en cada cas un exemple de violació en Java, la seva correcció i, sobretot, què implica el principi quan l'elevem del nivell de classe al nivell de mòduls i serveis.

Contingut

  1. Què significa SOLID i per què és arquitectònic
  2. S — Principi de Responsabilitat Única (SRP)
  3. O — Principi d'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. SOLID a nivell d'arquitectura
  8. Errors comuns i consells
  9. Exercicis
  10. Conclusió

  1. Què significa SOLID i per què és arquitectònic

Lletra Principi Idea central
S Responsabilitat Única Una classe ha de tenir una sola raó per canviar
O Obert/Tancat Obert a extensió, tancat a modificació
L Substitució de Liskov Els subtipus han de poder reemplaçar els seus tipus base
I Segregació d'Interfícies Interfícies petites i específiques, no monolítiques
D Inversió de Dependències Depèn d'abstraccions, no de concrecions

El fil conductor dels cinc és gestionar les dependències i les raons de canvi. A nivell de classe milloren el codi; a nivell d'arquitectura determinen com es dibuixen les fronteres entre mòduls i en quina direcció apunten les dependències.

  1. S — Principi de Responsabilitat Única (SRP)

Una classe ha de tenir una única raó per canviar.

"Raó per canviar" significa un únic actor o aspecte del negoci responsable de demanar modificacions. Si una classe atén diversos actors, els canvis d'un poden trencar el que necessita un altre.

// VIOLACIÓ: tres raons de canvi en una classe
public class Empleado {
    public double calcularNomina() { /* lògica de RRHH */ return 0; }
    public void guardar() { /* lògica de persistència (BD) */ }
    public String generarInformeHoras() { /* lògica de reporting */ return ""; }
}

Explicació del problema: calcularNomina canvia si canvien les regles de RRHH; guardar canvia si canvia l'esquema de la base de dades; generarInformeHoras canvia si canvia el format dels informes. Tres actors diferents, una sola classe: qualsevol canvi arrisca a trencar els altres.

// CORRECCIÓ: una responsabilitat per classe
public class Empleado {
    private double salarioBase;
    private double horasTrabajadas;
    // només dades i regles pròpies de l'empleat
}

public class CalculadoraNomina {
    public double calcular(Empleado e) { /* regles de RRHH */ return 0; }
}

public class RepositorioEmpleados {
    public void guardar(Empleado e) { /* persistència */ }
}

public class GeneradorInformeHoras {
    public String generar(Empleado e) { /* reporting */ return ""; }
}

Cada classe té ara un únic motiu per canviar i un únic responsable de negoci al darrere.

  1. O — Principi d'Obert/Tancat (OCP)

Les entitats han d'estar obertes a l'extensió però tancades a la modificació.

Hauríem de poder afegir comportament nou sense tocar el codi ja provat i en producció. L'eina habitual és el polimorfisme.

// VIOLACIÓ: cada nou tipus obliga a modificar el mètode
public class CalculadoraArea {
    public double calcular(Object figura) {
        if (figura instanceof Circulo) {
            Circulo c = (Circulo) figura;
            return Math.PI * c.radio * c.radio;
        } else if (figura instanceof Rectangulo) {
            Rectangulo r = (Rectangulo) figura;
            return r.ancho * r.alto;
        }
        return 0;
    }
}

Explicació del problema: afegir un Triangulo exigeix modificar calcular, recompilar i tornar a provar-ho tot. El mètode creix sense límit i concentra el risc.

// CORRECCIÓ: extensió per noves classes, sense modificar les existents
public interface Figura {
    double area();
}
public class Circulo implements Figura {
    private final double radio;
    public Circulo(double radio) { this.radio = radio; }
    public double area() { return Math.PI * radio * radio; }
}
public class Rectangulo implements Figura {
    private final double ancho, alto;
    public Rectangulo(double ancho, double alto) { this.ancho = ancho; this.alto = alto; }
    public double area() { return ancho * alto; }
}
// Per afegir un Triangulo: nova classe que implementa Figura. Res més canvia.

  1. L — Principi de Substitució de Liskov (LSP)

Si S és un subtipus de T, els objectes de tipus T han de poder substituir-se per objectes de tipus S sense alterar la correcció del programa.

Heretar no n'hi ha prou: la subclasse ha de respectar el contracte de la superclasse. L'exemple clàssic és el rectangle-quadrat.

// VIOLACIÓ: Cuadrado trenca el contracte de Rectangulo
public class Rectangulo {
    protected int ancho, alto;
    public void setAncho(int a) { this.ancho = a; }
    public void setAlto(int a) { this.alto = a; }
    public int area() { return ancho * alto; }
}
public class Cuadrado extends Rectangulo {
    public void setAncho(int a) { this.ancho = a; this.alto = a; }
    public void setAlto(int a) { this.ancho = a; this.alto = a; }
}

Explicació del problema: un codi que espera un Rectangulo assumeix que en fixar amplada=5 i alçada=4 l'àrea és 20. Amb un Cuadrado l'àrea seria 16. La subclasse viola l'expectativa del client; el polimorfisme es torna traïdor.

// CORRECCIÓ: modelar el que comparteixen, no forçar l'herència
public interface Figura {
    int area();
}
public final class Rectangulo implements Figura {
    private final int ancho, alto;
    public Rectangulo(int ancho, int alto) { this.ancho = ancho; this.alto = alto; }
    public int area() { return ancho * alto; }
}
public final class Cuadrado implements Figura {
    private final int lado;
    public Cuadrado(int lado) { this.lado = lado; }
    public int area() { return lado * lado; }
}

Regles pràctiques per no trencar LSP: no enfortir precondicions, no debilitar postcondicions, no llançar excepcions noves inesperades i no canviar el significat del contracte.

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

Cap client no hauria de veure's obligat a dependre de mètodes que no fa servir.

Les interfícies "grasses" obliguen les implementacions a carregar amb mètodes irrellevants.

// VIOLACIÓ: interfície monolítica
public interface Trabajador {
    void trabajar();
    void comer();
    void cobrarSalario();
}
// Un robot ha d'implementar comer() sense sentit
public class Robot implements Trabajador {
    public void trabajar() { /* ok */ }
    public void comer() { throw new UnsupportedOperationException(); }
    public void cobrarSalario() { throw new UnsupportedOperationException(); }
}

Explicació del problema: Robot es veu forçat a implementar comer() i cobrarSalario() que no li apliquen, la qual cosa produeix mètodes buits o que llancen excepcions (cosa que de passada viola LSP).

// CORRECCIÓ: interfícies petites i enfocades
public interface Trabajable { void trabajar(); }
public interface Alimentable { void comer(); }
public interface Asalariado { void cobrarSalario(); }

public class Humano implements Trabajable, Alimentable, Asalariado {
    public void trabajar() {}
    public void comer() {}
    public void cobrarSalario() {}
}
public class Robot implements Trabajable {
    public void trabajar() {}
}

Cada classe implementa només el que de debò necessita.

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

Els mòduls d'alt nivell no han de dependre dels de baix nivell; tots dos han de dependre d'abstraccions. Les abstraccions no han de dependre dels detalls.

És el principi més profundament arquitectònic: inverteix la direcció natural de les dependències.

// VIOLACIÓ: la lògica de negoci depèn d'un detall concret
public class ServicioFacturacion {
    private final RepositorioMySql repo = new RepositorioMySql(); // detall concret

    public void facturar(Factura f) {
        repo.guardar(f);
    }
}

Explicació del problema: la classe d'alt nivell (ServicioFacturacion) depèn directament d'un detall de baix nivell (RepositorioMySql). Canviar de MySql a PostgreSQL, o testejar sense BD, obliga a tocar el negoci.

// CORRECCIÓ: tots dos depenen d'una abstracció definida pel negoci
public interface RepositorioFacturas {        // abstracció (la defineix el domini)
    void guardar(Factura f);
}
public class ServicioFacturacion {            // alt nivell
    private final RepositorioFacturas repo;
    public ServicioFacturacion(RepositorioFacturas repo) { this.repo = repo; }
    public void facturar(Factura f) { repo.guardar(f); }
}
public class RepositorioMySql implements RepositorioFacturas { // baix nivell (detall)
    public void guardar(Factura f) { /* JDBC */ }
}

Explicació de la millora: la interfície RepositorioFacturas pertany conceptualment a la capa de negoci. El detall de MySql la implementa. Així la fletxa de dependència apunta cap al domini, no cap a la infraestructura: això és la inversió.

  1. SOLID a nivell d'arquitectura

En pujar de la classe al sistema, cada principi adquireix una lectura més àmplia:

Principi A nivell de classe A nivell d'arquitectura
SRP Una raó per canviar per classe Un servei/mòdul per capacitat de negoci (límits de context)
OCP Polimorfisme per estendre Plugins, extensions i feature flags sense tornar a desplegar el nucli
LSP Subtipus substituïbles Implementacions intercanviables d'un contracte (p. ex. proveïdors)
ISP Interfícies petites APIs i contractes de servei cohesionats; evitar APIs "grasses"
DIP Injecció de dependències Arquitectura hexagonal/neta: domini independent de la infraestructura

El DIP és la base de les arquitectures netes i hexagonals, on el domini defineix ports (interfícies) i la infraestructura aporta adaptadors. El diagrama següent ho il·lustra:

graph TD
    UI[Adaptador Web] --> P1[Port entrada]
    P1 --> D[Domini / Casos d'ús]
    D --> P2[Port sortida: RepositorioFacturas]
    DB[Adaptador MySql] -.implementa.-> P2

Explicació del diagrama: el domini està al centre i no apunta a cap detall. Els adaptadors (web, base de dades) depenen del domini a través de ports. La regla de dependència sempre apunta cap endins.

Errors Comuns i Consells

  • Sobreaplicar SRP fins a crear una explosió de microclasses anèmiques. El criteri és "una raó de canvi", no "una línia per classe".
  • Confondre OCP amb prohibir tot canvi. Sí que es modifica per corregir errors; el que s'evita és modificar per afegir variants previsibles.
  • Heretar per reutilitzar codi en lloc de per una relació "és-un" real: principal font de violacions de LSP. Prefereix la composició.
  • Crear una interfície per classe mecànicament (DIP mal entès). El DIP demana abstraccions on hi ha variació o frontera, no a tot arreu.
  • Col·locar les interfícies (ports) a la capa d'infraestructura. Perquè el DIP inverteixi de debò, l'abstracció ha de pertànyer al costat que la consumeix (el domini).
  • Consell: SOLID són guies, no lleis físiques. L'objectiu final és codi mantenible; si un principi et porta a més complexitat sense benefici, reconsidera'l.

Exercicis

Exercici 1 (SRP). Aquesta classe viola SRP. Indica les raons de canvi i proposa'n una separació:

public class GestorUsuario {
    public void registrar(Usuario u) { /* valida i guarda a BD */ }
    public void enviarBienvenida(Usuario u) { /* envia email */ }
}

Exercici 2 (OCP). Refactoritza per complir Obert/Tancat:

public class CalculadoraPrecio {
    public double precio(String tipoCliente, double base) {
        if (tipoCliente.equals("VIP")) return base * 0.8;
        if (tipoCliente.equals("NORMAL")) return base;
        return base;
    }
}

Exercici 3 (DIP). Quin principi viola aquest codi i com el corregeixes?

public class GeneradorReporte {
    private final ImpresoraEpson impresora = new ImpresoraEpson();
    public void imprimir(String r) { impresora.imprimir(r); }
}

Solucions

Solució 1. Hi ha dues raons de canvi: el registre/persistència i la comunicació per email. Separem:

public class ServicioRegistro {
    private final RepositorioUsuarios repo;
    public ServicioRegistro(RepositorioUsuarios repo) { this.repo = repo; }
    public void registrar(Usuario u) { /* valida */ repo.guardar(u); }
}
public class ServicioBienvenida {
    private final PasarelaEmail email;
    public ServicioBienvenida(PasarelaEmail email) { this.email = email; }
    public void enviar(Usuario u) { email.enviar(u.getEmail(), "Bienvenido"); }
}

Solució 2. Modelem el tipus de client com una estratègia polimòrfica:

public interface PoliticaPrecio {
    double aplicar(double base);
}
public class PrecioVip implements PoliticaPrecio {
    public double aplicar(double base) { return base * 0.8; }
}
public class PrecioNormal implements PoliticaPrecio {
    public double aplicar(double base) { return base; }
}
public class CalculadoraPrecio {
    public double precio(PoliticaPrecio politica, double base) {
        return politica.aplicar(base);
    }
}

Un nou tipus de client és una nova classe, sense tocar la calculadora.

Solució 3. Viola el DIP (i dificulta el testeig): depèn de la concreció ImpresoraEpson. Correcció:

public interface Impresora { void imprimir(String r); }
public class GeneradorReporte {
    private final Impresora impresora;
    public GeneradorReporte(Impresora impresora) { this.impresora = impresora; }
    public void imprimir(String r) { impresora.imprimir(r); }
}
public class ImpresoraEpson implements Impresora {
    public void imprimir(String r) { /* ... */ }
}

Conclusió

Els principis SOLID són cinc respostes concretes a la pregunta de com gestionar dependències i raons de canvi. SRP separa responsabilitats, OCP permet estendre sense trencar, LSP garanteix polimorfisme segur, ISP manté contractes enfocats i DIP inverteix les dependències per protegir el domini. Portats al pla arquitectònic, sustenten les arquitectures netes i hexagonals. Convé recordar que són guies al servei de la mantenibilitat, no fins en si mateixos. A la lliçó següent complementarem SOLID amb una altra família de principis més pragmàtics —DRY, KISS i YAGNI— que regulen l'equilibri entre rigor i simplicitat.

Curs d'Arquitectura d'Aplicacions

Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions

Mòdul 2: Principis i Tàctiques de Disseny

Mòdul 3: Estils i Patrons Arquitectònics

Mòdul 4: Arquitectures Distribuïdes i Microserveis

Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria

Mòdul 6: Disseny Dirigit pel Domini (DDD)

Mòdul 7: Dades i Persistència

Mòdul 8: Arquitectura al Núvol i Desplegament

Mòdul 9: Qualitat, Seguretat i Observabilitat

Mòdul 10: Evolució, Governança i Casos Pràctics

© Copyright 2026. Tots els drets reservats