Un cop traçades les fronteres estratègiques amb els Bounded Contexts, necessitem construir el model de domini dins de cada context. El disseny tàctic del DDD ens proporciona un conjunt de patrons d'implementació —Value Objects, Entitats, Agregats, Repositoris, Serveis de Domini i Esdeveniments de Domini— que ens permeten plasmar les regles del negoci en codi robust i expressiu. Aquesta lliçó és la més pràctica del mòdul: treballarem amb exemples en Java que veuràs reflectits en projectes reals. Dominar aquests patrons és el que diferencia un model anèmic (dades sense regles) d'un model ric que protegeix la integritat del negoci.
Contingut
- Value Objects (objectes de valor)
- Entitats
- Agregats i arrel d'agregat
- Repositoris
- Serveis de domini
- Esdeveniments de domini
- Value Objects (objectes de valor)
Un Value Object és un objecte que es defineix pels seus atributs, no per una identitat. Dos Value Objects amb els mateixos valors són intercanviables. Són immutables: un cop creats, no canvien.
Exemples típics: una quantitat de diners, una data, una adreça, un rang de dates.
public final class Dinero {
private final BigDecimal importe;
private final String moneda;
public Dinero(BigDecimal importe, String moneda) {
if (importe == null || moneda == null) {
throw new IllegalArgumentException("Importe i moneda són obligatoris");
}
this.importe = importe;
this.moneda = moneda;
}
public Dinero sumar(Dinero otro) {
if (!this.moneda.equals(otro.moneda)) {
throw new IllegalArgumentException("No es poden sumar monedes diferents");
}
return new Dinero(this.importe.add(otro.importe), this.moneda); // retorna NOU objecte
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Dinero)) return false;
Dinero d = (Dinero) o;
return importe.compareTo(d.importe) == 0 && moneda.equals(d.moneda);
}
@Override
public int hashCode() { return Objects.hash(importe, moneda); }
}Punts clau d'aquest Dinero:
- La classe és
finali tots els seus camps sónfinal: és immutable, no hi ha setters. - El mètode
sumarno modifica l'objecte actual; retorna un nouDinero. Aquesta és l'essència de la immutabilitat. - S'inclou una regla de negoci (no sumar monedes diferents) dins del mateix objecte.
- Se sobreescriuen
equalsihashCodebasant-se en els valors: dosDinerode 10 EUR són iguals encara que siguin instàncies diferents. Això és el que defineix un Value Object.
Avantatge pràctic: usar Dinero en lloc d'un BigDecimal solt evita errors com sumar euros amb dòlars o passar l'import sense la moneda.
- Entitats
Una Entitat és un objecte que té una identitat única i contínua en el temps, independent dels seus atributs. Encara que canviïn totes les seves dades, continua sent la mateixa entitat. Una Poliza, un Asegurado o un Siniestro són entitats: identificades pel seu número/ID, no pels seus atributs.
public class Siniestro {
private final IdSiniestro id; // identitat: no canvia mai
private EstadoSiniestro estado; // els atributs poden canviar
private Dinero importeReclamado;
public Siniestro(IdSiniestro id, Dinero importeReclamado) {
this.id = Objects.requireNonNull(id);
this.importeReclamado = Objects.requireNonNull(importeReclamado);
this.estado = EstadoSiniestro.ABIERTO;
}
public void cerrar() {
if (estado == EstadoSiniestro.CERRADO) {
throw new IllegalStateException("El sinistre ja està tancat");
}
this.estado = EstadoSiniestro.CERRADO;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Siniestro)) return false;
return this.id.equals(((Siniestro) o).id); // igualtat per IDENTITAT
}
@Override
public int hashCode() { return id.hashCode(); }
}Diferències amb el Value Object:
- La identitat
idésfinali defineix l'entitat. Encara que canviïnestadoiimporteReclamado, continua sent el mateix sinistre. equalsihashCodees basen només en l'id, no en tots els atributs. Dos sinistres amb el mateixidsón el mateix, encara que difereixin en estat.- L'entitat pot mutar (
cerrar()canvia l'estat), però sempre a través de mètodes que protegeixen les regles.
| Aspecte | Value Object | Entitat |
|---|---|---|
| Identitat | No en té; es defineix pels seus valors | Té un ID propi i estable |
| Mutabilitat | Immutable | Mutable (de manera controlada) |
| Igualtat | Per valors (equals sobre atributs) |
Per identitat (equals sobre l'ID) |
| Exemples | Diners, Data, Adreça | Pòlissa, Sinistre, Assegurat |
- Agregats i arrel d'agregat
Un Agregat és un grup d'entitats i Value Objects que es tracten com una unitat de consistència. Té una entitat principal anomenada arrel de l'agregat (Aggregate Root), que és l'única porta d'entrada a l'agregat: tot accés des de fora hi passa.
Regles fonamentals dels agregats:
- Només es referencia l'arrel des de fora. Els objectes interns no s'exposen directament.
- L'arrel protegeix les invariants (regles que sempre s'han de complir) de tot l'agregat.
- Un agregat es desa i es carrega complet, com una unitat transaccional.
- Entre agregats, les referències es fan per identitat (ID), no per objecte.
// Poliza és l'ARREL de l'agregat; Cobertura és una entitat interna
public class Poliza {
private final IdPoliza id;
private final List<Cobertura> coberturas = new ArrayList<>();
private Dinero primaTotal;
// L'accés a les cobertures passa SEMPRE per l'arrel
public void anadirCobertura(TipoCobertura tipo, Dinero capital) {
if (coberturas.size() >= 10) {
throw new IllegalStateException("Màxim 10 cobertures per pòlissa");
}
Cobertura c = new Cobertura(tipo, capital);
coberturas.add(c);
recalcularPrima(); // l'arrel manté la INVARIANT de coherència
}
private void recalcularPrima() {
Dinero total = new Dinero(BigDecimal.ZERO, "EUR");
for (Cobertura c : coberturas) {
total = total.sumar(c.calcularPrima());
}
this.primaTotal = total;
}
// S'exposa una còpia de només lectura, mai la llista interna mutable
public List<Cobertura> getCoberturas() {
return Collections.unmodifiableList(coberturas);
}
}Anàlisi detallada:
Polizaés l'arrel.Coberturaés una entitat interna que no es manipula directament des de fora.- Per afegir una cobertura, s'usa
anadirCobertura()a l'arrel, que aplica la invariant "màxim 10 cobertures" i recalcula la prima. Si deixéssim modificar la llista per fora, aquesta regla es podria trencar. recalcularPrima()garanteix que la prima total sempre sigui coherent amb les cobertures: aquesta és una invariant de l'agregat.getCoberturas()retorna una llista inmodificable: l'exterior pot llegir, però no alterar l'estat intern saltant-se les regles.
graph TD
subgraph Agregado_Poliza[Agregat: Poliza]
R[Poliza - ARREL] --> C1[Cobertura 1]
R --> C2[Cobertura 2]
R --> PT[primaTotal - Value Object]
end
EXT[Codi extern] -->|nomes accedeix a l'arrel| RAquest diagrama mostra que el codi extern només "veu" l'arrel Poliza; les cobertures i la prima són internes i protegides.
Regla d'or: una transacció modifica un sol agregat. Si necessites canviar-ne diversos, normalment és senyal que els has de coordinar amb esdeveniments de domini (apartat 6), no en la mateixa transacció.
- Repositoris
Un Repositori proporciona la il·lusió d'una col·lecció en memòria d'arrels d'agregat, ocultant els detalls de persistència (base de dades, etc.). Hi ha un repositori per agregat (per la seva arrel), no per cada entitat interna.
// La INTERFÍCIE viu a la capa de domini: parla de Poliza, no de SQL
public interface RepositorioPolizas {
Optional<Poliza> buscarPorId(IdPoliza id);
void guardar(Poliza poliza); // desa l'agregat COMPLET
List<Poliza> buscarVigentesDe(IdAsegurado asegurado);
}Comentaris sobre la interfície:
- Viu al domini i usa exclusivament conceptes del domini (
Poliza,IdPoliza). No menciona JDBC, JPA ni SQL: el domini no ha de saber com es persisteix. guardar(Poliza)persisteix l'agregat complet (la pòlissa amb les seves cobertures), respectant que l'agregat és la unitat de consistència.- Només exposa operacions amb sentit al domini (buscar pòlisses vigents d'un assegurat).
// La IMPLEMENTACIÓ viu a la capa d'infraestructura
@Repository
public class RepositorioPolizasJpa implements RepositorioPolizas {
private final EntityManager em;
public RepositorioPolizasJpa(EntityManager em) { this.em = em; }
@Override
public Optional<Poliza> buscarPorId(IdPoliza id) {
return Optional.ofNullable(em.find(Poliza.class, id.valor()));
}
@Override
public void guardar(Poliza poliza) { em.merge(poliza); }
@Override
public List<Poliza> buscarVigentesDe(IdAsegurado asegurado) {
return em.createQuery(
"SELECT p FROM Poliza p WHERE p.asegurado = :a AND p.vigente = true",
Poliza.class)
.setParameter("a", asegurado.valor())
.getResultList();
}
}El més important d'aquesta implementació:
- Implementa la interfície del domini però conté els detalls tècnics (JPA,
EntityManager, JPQL). Aquesta separació és el que permet canviar la base de dades sense tocar el domini. - És una aplicació directa del principi d'inversió de dependències: el domini defineix la interfície, la infraestructura la implementa.
- Serveis de domini
De vegades una operació del negoci no pertany de manera natural a cap entitat ni Value Object. Quan una lògica important del domini implica diversos agregats o conceptes, es modela com un Servei de Domini: un objecte sense estat que exposa una operació amb nom del negoci.
public class ServicioTarificacion {
private final TablaActuarial tabla;
public ServicioTarificacion(TablaActuarial tabla) { this.tabla = tabla; }
// Operació del domini que no encaixa en una sola entitat
public Dinero calcularPrima(PerfilRiesgo perfil, TipoCobertura cobertura) {
BigDecimal factor = tabla.factorPara(perfil.edad(), perfil.profesion());
BigDecimal base = cobertura.capitalAsegurado().importe();
return new Dinero(base.multiply(factor), "EUR");
}
}Per què això és un Servei de Domini i no un mètode d'una entitat:
- Calcular la prima combina un
PerfilRiesgo, unaTablaActuariali unTipoCobertura. No "pertany" a cap d'ells en exclusiva. - El servei no té estat propi: només orquestra el càlcul amb les dades que rep.
- El seu nom,
calcularPrima, és un verb del Llenguatge Ubic. No és un "Helper" ni un "Utils" genèric.
Compte: no abusis dels serveis de domini. Si tota la lògica acaba en serveis i les entitats queden buides, has tornat al model anèmic. Usa serveis només quan l'operació realment no càpiga en una entitat.
- Esdeveniments de domini
Un Esdeveniment de Domini representa quelcom rellevant que ha ocorregut al domini i que pot interessar a altres. Es nomena en passat: PolizaSuscrita, SiniestroCerrado. Permeten que diferents agregats (fins i tot de diferents Bounded Contexts) reaccionin sense acoblar-se directament.
// L'esdeveniment és immutable i descriu un fet consumat del passat
public final class PolizaSuscrita {
private final IdPoliza idPoliza;
private final IdAsegurado idAsegurado;
private final Instant fecha;
public PolizaSuscrita(IdPoliza idPoliza, IdAsegurado idAsegurado) {
this.idPoliza = idPoliza;
this.idAsegurado = idAsegurado;
this.fecha = Instant.now();
}
// getters de només lectura...
}// L'arrel publica l'esdeveniment quan ocorre el fet
public class Poliza {
private final List<Object> eventos = new ArrayList<>();
public void suscribir(IdAsegurado asegurado) {
// ... lògica de subscripció i validació d'invariants ...
this.eventos.add(new PolizaSuscrita(this.id, asegurado)); // registra l'esdeveniment
}
public List<Object> eventosPendientes() {
return Collections.unmodifiableList(eventos);
}
}Com funciona i per què importa:
- Quan s'executa
suscribir(), la pòlissa registra un esdevenimentPolizaSuscritaen lloc de cridar directament altres mòduls. - Després de desar l'agregat, la infraestructura publica aquests esdeveniments. Altres contextos (per exemple, Facturació o Notificacions) es poden subscriure i reaccionar (emetre el primer rebut, enviar la benvinguda).
- Això desacobla els agregats: la
Polizano sap ni li importa qui reacciona a la seva subscripció. És la base de les arquitectures dirigides per esdeveniments.
Representat com a flux:
sequenceDiagram
participant U as Usuari
participant P as Poliza (Subscripcio)
participant B as Facturacio
participant N as Notificacions
U->>P: suscribir()
P->>P: registra PolizaSuscrita
P-->>B: PolizaSuscrita
P-->>N: PolizaSuscrita
B->>B: emet primer rebut
N->>N: envia email de benvingudaEl diagrama de seqüència mostra com un únic esdeveniment PolizaSuscrita desencadena reaccions independents a Facturació i Notificacions, sense que Subscripció conegui aquests detalls.
Errors Comuns i Consells
- Agregats massa grans. Ficar mitja base de dades dins d'un sol agregat genera bloquejos i problemes de rendiment. Mantén els agregats petits; referencia altres agregats per ID.
- Modificar diversos agregats en una transacció. Trenca la regla de consistència. Usa esdeveniments de domini per coordinar canvis entre agregats.
- Repositoris per entitat interna. Només les arrels d'agregat tenen repositori. No creïs un
RepositorioCoberturassiCoberturaviu dins de l'agregatPoliza. - Value Objects mutables. Si afegeixes setters a un Value Object, en perds les garanties. Mantén-los immutables i retorna còpies noves a les operacions.
- Abusar dels serveis de domini. Buiden les entitats i reapareix el model anèmic. Pregunta't sempre si la lògica càpiga en una entitat abans de crear un servei.
- Consell: comença pels Value Objects. Substituir primitius (
BigDecimal,String,int) per conceptes del domini (Dinero,Email,IdPoliza) ja millora enormement l'expressivitat i seguretat del model.
Exercicis
Exercici 1. Decideix, per a cada concepte del domini d'una biblioteca, si el modelaries com a Entitat o com a Value Object, justificant-ho: (a) un exemplar físic d'un llibre; (b) un ISBN; (c) un usuari de la biblioteca; (d) un període de préstec (data d'inici i fi).
Exercici 2. Donat un agregat Pedido que conté LineaPedido, escriu la signatura d'un mètode a l'arrel que afegeixi una línia respectant la invariant "el total de la comanda no pot superar 5.000 EUR". Explica per què el mètode va a l'arrel i no a LineaPedido.
Exercici 3. Identifica quin patró tàctic (Value Object, Entitat, Servei de Domini, Esdeveniment de Domini o Repositori) usaries per a cada cas: (a) desar i recuperar comandes; (b) representar un percentatge de descompte; (c) transferir saldo entre dos comptes; (d) notificar que "una comanda ha estat enviada".
Solucions
Solució 1. (a) Entitat: cada exemplar físic té identitat pròpia (se'n pot perdre o deteriorar un de concret). (b) Value Object: un ISBN es defineix pel seu valor; dos ISBN iguals són intercanviables. (c) Entitat: l'usuari té identitat estable encara que canviïn les seves dades. (d) Value Object: un període és immutable i es defineix per les seves dates.
Solució 2. Una signatura vàlida: public void anadirLinea(Producto producto, int cantidad). El mètode va a l'arrel Pedido perquè la invariant "el total no supera 5.000 EUR" depèn de totes les línies en conjunt. Una sola LineaPedido no coneix el total de la comanda, així que no pot garantir aquesta regla; només l'arrel, que veu el conjunt complet, ho pot fer.
Solució 3.
(a) Repositori (de l'arrel Pedido).
(b) Value Object (un percentatge immutable, definit pel seu valor).
(c) Servei de Domini (la transferència implica dos agregats Cuenta i no pertany a un de sol).
(d) Esdeveniment de Domini (PedidoEnviado, un fet consumat del passat).
Conclusió
En aquesta lliçó hem recorregut l'arsenal tàctic del DDD: els Value Objects immutables definits pels seus valors, les Entitats amb identitat estable, els Agregats que agrupen objectes sota una arrel que protegeix les invariants, els Repositoris que oculten la persistència, els Serveis de Domini per a la lògica que no encaixa en una entitat, i els Esdeveniments de Domini que desacoblen reaccions entre parts del sistema. Amb aquests patrons pots construir un model ric i fidel al negoci dins de cada Bounded Context.
Fins ara hem vist els contextos per separat. A l'última lliçó del mòdul, "Mapatge de Contextos (Context Mapping)", estudiarem com es relacionen i s'integren els diferents Bounded Contexts entre si mitjançant patrons com ACL, Shared Kernel o Open Host Service.
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
