Quan construïm programari, la diferència entre un sistema que evoluciona amb facilitat i un altre que es converteix en un llast rarament rau en l'algorisme concret que escrivim. Rau en com organitzem les peces i en com depenen les unes de les altres. L'acoblament i la cohesió són les dues mètriques conceptuals més importants per raonar sobre aquesta organització: ens diuen com d'entrellaçats estan els nostres mòduls i com d'enfocada està cada peça en una única tasca. Dominar aquests conceptes és el primer pas per prendre decisions arquitectòniques conscients en lloc d'acumular complexitat accidental. En aquesta lliçó els estudiarem en profunditat, veurem com es manifesten en codi Java real i com refactoritzar per millorar-los, tot acabant amb una regla pràctica fonamental: la Llei de Demeter.

Contingut

  1. Acoblament: què és i per què importa
  2. Tipus d'acoblament (de pitjor a millor)
  3. Cohesió: alta enfront de baixa
  4. La relació entre acoblament i cohesió
  5. Separació de responsabilitats (SoC)
  6. Refactorització guiada: d'alt acoblament a baix acoblament
  7. La Llei de Demeter (principi de mínim coneixement)
  8. Errors comuns i consells
  9. Exercicis
  10. Conclusió

  1. Acoblament: què és i per què importa

L'acoblament mesura el grau d'interdependència entre dos mòduls: quant necessita conèixer un mòdul sobre els detalls interns d'un altre per funcionar. Quan dos components estan fortament acoblats, un canvi en un obliga a canviar l'altre.

Conceptes clau:

  • Baix acoblament (desitjable): els mòduls es comuniquen a través d'interfícies estables i mínimes. Un canvi intern en un mòdul no afecta els altres.
  • Alt acoblament (problemàtic): els mòduls depenen de detalls interns, tipus concrets o estructures de dades d'altres mòduls.
  • L'acoblament mai no pot ser zero: si els mòduls no es comuniquessin, no formarien un sistema. L'objectiu és minimitzar-lo i fer-lo explícit.

Conseqüències de l'alt acoblament:

  • Fragilitat: canvis localitzats provoquen errors en cascada en llocs inesperats.
  • Rigidesa: és difícil modificar el sistema perquè cada canvi n'arrossega altres.
  • Baixa reutilització: no es pot extreure un mòdul sense arrossegar-ne les dependències.
  • Dificultat per testejar: no es pot provar un mòdul de manera aïllada.

  1. Tipus d'acoblament (de pitjor a millor)

Històricament (Stevens, Myers i Constantine, 1974) l'acoblament es classifica en una escala. Aquesta taula els ordena del més nociu al més sa:

Tipus Descripció Valoració
De contingut Un mòdul modifica o depèn de les dades internes d'un altre Molt dolent
Comú Diversos mòduls comparteixen estat global mutable Dolent
Extern Dependència d'un format o protocol extern imposat Dolent
De control Un mòdul controla el flux d'un altre passant-li banderes Regular
De marca (stamp) Es passa una estructura completa quan només se'n necessita una part Millorable
De dades Es passen només les dades estrictament necessàries Bo
De missatge Comunicació només mitjançant missatges/interfícies sense paràmetres interns Òptim

Exemple d'acoblament de control (un dels més freqüents i evitables):

// MALAMENT: qui crida controla el flux intern mitjançant una bandera
public class GeneradorInformes {
    public String generar(Datos datos, boolean esPdf) {
        if (esPdf) {
            return generarPdf(datos);
        } else {
            return generarHtml(datos);
        }
    }
}

Explicació del problema: el paràmetre boolean esPdf és una bandera de control. El codi que crida ha de conèixer la lògica interna del mètode per saber què fa cada valor. A més, cada nou format (CSV, XML) obliga a afegir paràmetres o branques, tot trencant el mètode existent.

// BÉ: acoblament de dades/missatge mitjançant polimorfisme
public interface GeneradorInformes {
    String generar(Datos datos);
}

public class GeneradorPdf implements GeneradorInformes {
    public String generar(Datos datos) { /* ... */ return "pdf"; }
}

public class GeneradorHtml implements GeneradorInformes {
    public String generar(Datos datos) { /* ... */ return "html"; }
}

Explicació de la millora: ara qui crida tria una implementació concreta i la utilitza a través de la interfície GeneradorInformes. No en coneix els detalls interns. Afegir un nou format consisteix a crear una nova classe, sense tocar les existents.

  1. Cohesió: alta enfront de baixa

La cohesió mesura com de relacionats i enfocats estan els elements dins d'un mateix mòdul. Una classe amb alta cohesió fa una sola cosa bé; una amb baixa cohesió barreja responsabilitats dispars.

Tipus de cohesió (de pitjor a millor):

Tipus Què agrupa Valoració
Coincidental Elements sense relació, agrupats a l'atzar Molt dolenta
Lògica Tasques similars per categoria però diferents en propòsit Dolenta
Temporal Coses que s'executen al mateix moment (p. ex. arrencada) Regular
Procedimental Passos d'un procediment, en cert ordre Millorable
Comunicacional Operacions sobre les mateixes dades Bona
Funcional Tot contribueix a una única tasca ben definida Òptima

Exemple de baixa cohesió:

// MALAMENT: una classe que ho fa tot (classe "Déu")
public class Utilidades {
    public void guardarUsuario(Usuario u) { /* accés a BD */ }
    public String formatearFecha(Date d) { /* format */ return ""; }
    public void enviarEmail(String destino) { /* SMTP */ }
    public double calcularImpuesto(double base) { return base * 0.21; }
}

Explicació del problema: aquesta classe agrupa accés a base de dades, formatatge, enviament de correu i càlcul fiscal. No hi ha cap relació entre aquestes tasques: és cohesió coincidental. Qualsevol desenvolupador que necessiti una sola d'aquestes funcions queda acoblat a tota la classe.

// BÉ: cada classe té una responsabilitat única (cohesió funcional)
public class RepositorioUsuarios {
    public void guardar(Usuario u) { /* accés a BD */ }
}

public class ServicioEmail {
    public void enviar(String destino, String cuerpo) { /* SMTP */ }
}

public class CalculadoraImpuestos {
    public double calcular(double base) { return base * 0.21; }
}

  1. La relació entre acoblament i cohesió

Aquestes dues mètriques solen moure's en direccions oposades i constitueixen el principi rector del bon disseny modular:

Objectiu: alta cohesió interna i baix acoblament extern.

graph LR
    subgraph "Disseny deficient"
        A1[Mòdul A] <--> B1[Mòdul B]
        A1 <--> C1[Mòdul C]
        B1 <--> C1
    end
    subgraph "Bon disseny"
        A2[Mòdul A] --> I[Interfície]
        B2[Mòdul B] --> I
        C2[Mòdul C] --> I
    end

Explicació del diagrama: a l'esquerra, tots els mòduls es coneixen entre si directament (xarxa densa = alt acoblament). A la dreta, els mòduls només depenen d'una interfície estable: les dependències són poques i dirigides. Si un mòdul canvia internament, mentre en respecti la interfície, els altres no se n'assabenten.

  1. Separació de responsabilitats (SoC)

La separació de responsabilitats (Separation of Concerns) és el principi que sustenta tant l'alta cohesió com el baix acoblament: cada part del sistema ha d'ocupar-se d'un únic aspecte ("concern") i res més.

Aspectes típics que convé separar:

  • Presentació (interfície d'usuari, serialització de respostes).
  • Lògica de negoci (regles del domini).
  • Persistència (accés a dades).
  • Aspectes transversals (logging, seguretat, transaccions).

Un patró clàssic de SoC és l'arquitectura en capes:

// Capa de presentació: només orquestra i tradueix HTTP <-> domini
@RestController
public class PedidoController {
    private final ServicioPedidos servicio;
    public PedidoController(ServicioPedidos servicio) { this.servicio = servicio; }

    @PostMapping("/pedidos")
    public RespuestaPedido crear(@RequestBody PeticionPedido peticion) {
        Pedido pedido = servicio.crearPedido(peticion.getItems());
        return RespuestaPedido.desde(pedido);
    }
}

// Capa de negoci: regles del domini, sense saber res d'HTTP ni de SQL
public class ServicioPedidos {
    private final RepositorioPedidos repositorio;
    public ServicioPedidos(RepositorioPedidos repositorio) { this.repositorio = repositorio; }

    public Pedido crearPedido(List<Item> items) {
        Pedido pedido = new Pedido(items);
        pedido.validar();
        return repositorio.guardar(pedido);
    }
}

Explicació: el PedidoController només coneix el protocol web i delega immediatament. El ServicioPedidos conté les regles i no sap si l'invoca un controlador REST, una cua de missatges o un test. Cada capa pot evolucionar i provar-se de manera independent.

  1. Refactorització guiada: d'alt acoblament a baix acoblament

Vegem una refactorització completa. Partim d'una classe que crea les seves pròpies dependències internament:

// ABANS: acoblament fort a implementacions concretes
public class ServicioNotificaciones {
    private final ClienteSmtp smtp = new ClienteSmtp("smtp.empresa.com");

    public void notificar(String usuario, String mensaje) {
        String email = new RepositorioUsuariosMySql().buscarEmail(usuario);
        smtp.enviar(email, mensaje);
    }
}

Problemes detectats:

  • Crea ClienteSmtp i RepositorioUsuariosMySql amb new: impossible substituir-los per dobles de prova.
  • Depèn de classes concretes, no d'abstraccions.
  • No es pot testejar sense un servidor SMTP i una base de dades reals.
// DESPRÉS: injecció de dependències contra interfícies
public interface PasarelaEmail {
    void enviar(String destino, String cuerpo);
}

public interface RepositorioUsuarios {
    String buscarEmail(String usuario);
}

public class ServicioNotificaciones {
    private final PasarelaEmail email;
    private final RepositorioUsuarios usuarios;

    // Les dependències es reben des de fora (inversió de control)
    public ServicioNotificaciones(PasarelaEmail email, RepositorioUsuarios usuarios) {
        this.email = email;
        this.usuarios = usuarios;
    }

    public void notificar(String usuario, String mensaje) {
        String destino = usuarios.buscarEmail(usuario);
        email.enviar(destino, mensaje);
    }
}

Explicació de la millora pas a pas:

  1. Definim interfícies (PasarelaEmail, RepositorioUsuarios) que descriuen què es necessita, no com s'implementa.
  2. El servei rep els seus col·laboradors pel constructor (injecció de dependències). Ja no fa servir new.
  3. En producció injectarem les implementacions reals; als tests, dobles lleugers. Això redueix l'acoblament a la seva forma de missatge i permet el testeig aïllat.

  1. La Llei de Demeter (principi de mínim coneixement)

La Llei de Demeter és una regla heurística que limita l'acoblament: "parla només amb els teus amics propers, no amb desconeguts". Un mètode d'un objecte només hauria d'invocar mètodes de:

  • el mateix objecte (this),
  • objectes que rep com a paràmetres,
  • objectes que crea ell mateix,
  • els seus atributs directes.

El símptoma de violació més visible és l'encadenament de crides (a.getB().getC().hacer()), també anomenat "tren de crides".

// VIOLA la Llei de Demeter: naveguem per l'estructura interna d'altres objectes
public class CalculadoraDescuento {
    public double calcular(Pedido pedido) {
        String pais = pedido.getCliente().getDireccion().getPais();
        if (pais.equals("ES")) return 0.10;
        return 0.0;
    }
}

Explicació del problema: CalculadoraDescuento coneix l'estructura interna de Pedido, de Cliente i de Direccion. Si qualsevol d'aquestes classes canvia la seva estructura, aquest mètode es trenca. L'acoblament és transitiu i ocult.

// COMPLEIX la Llei de Demeter: cada objecte exposa el que necessita
public class Pedido {
    private final Cliente cliente;
    public boolean esNacional() { return cliente.esNacional(); }
}

public class Cliente {
    private final Direccion direccion;
    public boolean esNacional() { return direccion.esEnPais("ES"); }
}

public class CalculadoraDescuento {
    public double calcular(Pedido pedido) {
        return pedido.esNacional() ? 0.10 : 0.0;
    }
}

Explicació de la millora: en lloc de demanar dades i decidir fora ("ask"), diem a l'objecte que faci la pregunta de negoci que li correspon ("tell"). Cada classe amaga la seva estructura interna. Això es coneix com a principi "Tell, Don't Ask" i és la cara pràctica de la Llei de Demeter.

Matís important: la Llei de Demeter s'aplica a objectes amb comportament de domini. No s'ha d'aplicar de manera dogmàtica a APIs fluides o builders (new Builder().con(x).con(y).build()), on l'encadenament és intencional i cada mètode retorna el mateix tipus.

Errors Comuns i Consells

  • Confondre poques línies amb alta cohesió. Una classe petita pot continuar sent incoherent si barreja aspectes. La cohesió és semàntica, no de mida.
  • Crear interfícies per a tot "per si de cas". Una abstracció innecessària és complexitat accidental. Introdueix interfícies quan hi hagi una variació real o una frontera de testeig.
  • Amagar l'acoblament en estat global o singletons. L'acoblament comú (estat compartit mutable) és dels més difícils de depurar; prefereix passar dependències explícites.
  • Aplicar la Llei de Demeter a estructures de dades pures (DTO). Accedir a camps d'un DTO sense lògica no la viola; la llei protegeix objectes amb comportament.
  • Consell: davant d'un canvi, observa quants fitxers has de tocar. Si són molts per a un canvi conceptualment petit, tens un problema d'acoblament o cohesió.
  • Consell: fes servir la regla "Tell, Don't Ask" com a detector ràpid de violacions de Demeter.

Exercicis

Exercici 1. Identifica el tipus d'acoblament i refactoritza:

public class Calculadora {
    public double operar(double a, double b, int tipo) {
        if (tipo == 1) return a + b;
        if (tipo == 2) return a - b;
        if (tipo == 3) return a * b;
        return 0;
    }
}

Exercici 2. La classe següent té baixa cohesió. Separa-la en classes amb responsabilitat única:

public class GestorTienda {
    public void procesarPago(double importe) { /* ... */ }
    public void actualizarInventario(String producto, int cantidad) { /* ... */ }
    public void registrarLog(String mensaje) { /* ... */ }
}

Exercici 3. Reescriu aquest mètode perquè compleixi la Llei de Demeter:

public double precioConEnvio(Factura factura) {
    return factura.getPedido().getTotal() + factura.getPedido().getEnvio().getCoste();
}

Solucions

Solució 1. És acoblament de control (l'int tipo dirigeix el flux). Refactoritzem a polimorfisme:

public interface Operacion {
    double aplicar(double a, double b);
}
public class Suma implements Operacion {
    public double aplicar(double a, double b) { return a + b; }
}
public class Resta implements Operacion {
    public double aplicar(double a, double b) { return a - b; }
}
public class Multiplicacion implements Operacion {
    public double aplicar(double a, double b) { return a * b; }
}
// Ús: Operacion op = new Suma(); double r = op.aplicar(2, 3);

Ara afegir operacions no requereix modificar codi existent i desapareixen les banderes.

Solució 2. Separem en tres classes amb cohesió funcional:

public class ServicioPagos {
    public void procesarPago(double importe) { /* ... */ }
}
public class ServicioInventario {
    public void actualizar(String producto, int cantidad) { /* ... */ }
}
public class ServicioRegistro {
    public void registrar(String mensaje) { /* ... */ }
}

Cada servei pot evolucionar, provar-se i reutilitzar-se de manera independent.

Solució 3. Encapsulem el càlcul dins dels objectes que posseeixen les dades:

public class Pedido {
    private double total;
    private Envio envio;
    public double precioTotalConEnvio() { return total + envio.getCoste(); }
}
public double precioConEnvio(Factura factura) {
    return factura.precioTotalConEnvio(); // Factura delega en el seu Pedido
}

Qui crida ja no navega per l'estructura interna; demana directament la dada de negoci.

Conclusió

En aquesta lliçó hem vist que el bon disseny es redueix, en bona mesura, a una idea: maximitzar la cohesió i minimitzar l'acoblament. Hem classificat els tipus d'acoblament i cohesió, hem entès que la separació de responsabilitats és el principi que els habilita, i hem practicat refactoritzacions reals fent servir interfícies i injecció de dependències. Finalment, la Llei de Demeter ens ha donat una regla operativa per detectar dependències ocultes. Aquests conceptes són la base sobre la qual es construeixen els principis SOLID, que estudiarem a la lliçó següent i que no són més que formulacions concretes i accionables d'aquestes mateixes idees.

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