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
- Acoblament: què és i per què importa
- Tipus d'acoblament (de pitjor a millor)
- Cohesió: alta enfront de baixa
- La relació entre acoblament i cohesió
- Separació de responsabilitats (SoC)
- Refactorització guiada: d'alt acoblament a baix acoblament
- La Llei de Demeter (principi de mínim coneixement)
- Errors comuns i consells
- Exercicis
- Conclusió
- 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.
- 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.
- 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; }
}
- 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
endExplicació 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.
- 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.
- 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
ClienteSmtpiRepositorioUsuariosMySqlambnew: 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:
- Definim interfícies (
PasarelaEmail,RepositorioUsuarios) que descriuen què es necessita, no com s'implementa. - El servei rep els seus col·laboradors pel constructor (injecció de dependències). Ja no fa servir
new. - 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.
- 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
- Què és l'Arquitectura d'Aplicacions?
- El Rol de l'Arquitecte de Programari
- Atributs de Qualitat i Requisits No Funcionals
- Decisions Arquitectòniques i Compromisos (Trade-offs)
- Documentació d'Arquitectura: Vistes i el Model C4
Mòdul 2: Principis i Tàctiques de Disseny
- Acoblament, Cohesió i Separació de Responsabilitats
- Principis SOLID Aplicats a l'Arquitectura
- DRY, KISS, YAGNI i Altres Principis de Disseny
- Tàctiques Arquitectòniques per als Atributs de Qualitat
- Gestió del Deute Tècnic
Mòdul 3: Estils i Patrons Arquitectònics
- Arquitectura Monolítica
- Arquitectura en Capes (N-Tier)
- Arquitectura Client-Servidor
- Arquitectura Hexagonal (Ports i Adaptadors)
- Arquitectura Neta i Ceba (Clean & Onion)
Mòdul 4: Arquitectures Distribuïdes i Microserveis
- Introducció als Sistemes Distribuïts
- Arquitectura de Microserveis
- Descomposició de Serveis i Bounded Contexts
- API Gateway, Service Discovery i Comunicació entre Serveis
- Patrons de Resiliència: Circuit Breaker, Retry i Bulkhead
- El Teorema CAP i la Consistència de Dades
Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria
- Fonaments de l'Arquitectura Orientada a Esdeveniments
- Missatgeria Asíncrona: Cues i Brokers
- Patrons d'Esdeveniments: Event Sourcing i CQRS
- Gestió de Transaccions Distribuïdes: Patró Saga
- Streaming de Dades en Temps Real
Mòdul 6: Disseny Dirigit pel Domini (DDD)
- Conceptes Fonamentals del DDD
- Disseny Estratègic: Bounded Contexts i Llenguatge Ubic
- Disseny Tàctic: Entitats, Agregats i Repositoris
- Mapatge de Contextos (Context Mapping)
Mòdul 7: Dades i Persistència
- Estratègies de Persistència: SQL vs NoSQL
- Patrons d'Accés a Dades: Repository, Unit of Work i DAO
- Base de Dades per Servei i Gestió de Dades Distribuïdes
- Cau i Estratègies d'Invalidació
Mòdul 8: Arquitectura al Núvol i Desplegament
- Fonaments del Cloud Computing (IaaS, PaaS, SaaS)
- Contenidors i Orquestració amb Docker i Kubernetes
- Arquitectura Serverless
- Patrons de Disseny Cloud-Native
- Infraestructura com a Codi (IaC)
Mòdul 9: Qualitat, Seguretat i Observabilitat
- Escalabilitat: Horitzontal vs Vertical i Balanceig de Càrrega
- Alta Disponibilitat i Tolerància a Fallades
- Seguretat per Disseny i Autenticació/Autorització
- Observabilitat: Logging, Mètriques i Traçabilitat
- Rendiment i Proves de Càrrega
