Resumen y ejemplo sobre Domain-Driven Design (DDD)
El Domain-Driven Design (DDD) es
un enfoque para el desarrollo de software que prioriza el dominio del negocio
sobre la tecnología. Propone una estrecha colaboración entre expertos técnicos
y de dominio para crear un modelo que refleje fielmente los procesos de
negocio.
Conceptos
clave de DDD
- Lenguaje Ubicuo: Vocabulario común compartido entre
desarrolladores y expertos del dominio.
- Modelo de Dominio: Representación abstracta del dominio del
negocio y sus reglas.
- Contextos Delimitados: Fronteras explícitas donde un modelo
particular es válido.
- Entidades, Value Objects, Agregados: Bloques de construcción del modelo.
- Servicios de Dominio: Operaciones que no pertenecen naturalmente a
entidades o value objects.
- Eventos de Dominio: Notificaciones de cambios significativos en
el dominio.
- Repositorios: Abstracción para persistencia de agregados.
Cómo
implementar DDD con éxito
- Inmersión en el dominio:
- Organiza sesiones de Event Storming con
expertos del dominio
- Documenta procesos y terminología del negocio
- Identifica subdominos (core, supporting,
generic)
- Diseño del modelo:
- Define contextos delimitados con claridad
- Establece un lenguaje ubicuo documentado
- Identifica entidades, value objects y
agregados
- Diseña reglas de negocio en el dominio, no en
aplicación
- Arquitectura hexagonal:
- Separa el dominio de la infraestructura
- Implementa puertos y adaptadores
- Usa inyección de dependencias
- Implementación práctica:
- Crea clases limpias con responsabilidades
claras
- Implementa validaciones a nivel de dominio
- Usa eventos para comunicación entre agregados
- Mantén agregados pequeños y cohesivos
- Refinamiento continuo:
- Refactoriza según evoluciona el conocimiento
del dominio
- Revisa regularmente con expertos del dominio
- Actualiza el lenguaje ubicuo según sea
necesario
Voy a crear un diagrama completo
para ilustrar estos conceptos en un ejemplo práctico.
Arquitectura
hexagonal
Contexto y
lenguaje ubicuo
Explicación
del Ejemplo Práctico
He
creado un ejemplo completo de una aplicación para gestión de cursos educativos
usando Domain-Driven Design. Revisemos cada diagrama y código para entender
cómo implementarlo correctamente:
1.
Modelo de Dominio
El
diagrama de clases muestra la estructura del modelo de dominio con:
- Entidades: Curso, Estudiante, Inscripción
- Value Objects: CursoId, EstudianteId,
InscripcionId, Precio
- Enumeraciones: EstadoInscripcion
- Repositorios: Interfaces para persistencia
- Eventos: InscripcionCompletadaEvent
Observa
cómo cada entidad tiene su propia identidad (ID) implementada como value
object, y cómo se establecen relaciones entre agregados mediante referencias
por ID.
2.
Arquitectura Hexagonal
El
segundo diagrama muestra la arquitectura hexagonal (o de puertos y adaptadores)
que complementa perfectamente el DDD:
- Centro: El dominio contiene todas las
reglas de negocio
- Capa de Aplicación: Orquesta casos de uso usando el
dominio
- Infraestructura: Implementa interfaces del dominio
(adaptadores secundarios)
- Interfaces de Usuario: Puntos de entrada a la aplicación
(adaptadores primarios)
Esta
separación permite que el dominio se mantenga puro y libre de dependencias
externas.
3.
Contextos Delimitados y Lenguaje Ubicuo
El
tercer diagrama muestra cómo dividir un sistema complejo en contextos
delimitados:
- Gestión de Cursos
- Gestión de Estudiantes
- Inscripciones
- Pagos
Cada
contexto tiene su propio modelo y terminología. Las líneas punteadas
representan los mapeos y traducciones necesarios entre contextos.
4.
Implementación en Código
El
código de ejemplo muestra:
- Value
Objects inmutables (CursoId, Precio)
- Entidades con identidad y
comportamiento
(Curso)
- Reglas de negocio encapsuladas dentro del dominio
- Invariantes protegidos con validaciones
- Interfaces de repositorio definidas en el dominio
- Servicios de dominio para orquestar operaciones entre
agregados
- Eventos de dominio para comunicación entre agregados
- Capas de aplicación que implementan casos de uso
- Adaptadores que conectan con el mundo exterior
Pasos para
implementar DDD con éxito
- Comienza con Event Storming: Reúne expertos en dominio y
técnicos para mapear eventos clave.
- Define contextos delimitados: Identifica los límites naturales
en tu dominio.
- Construye el lenguaje ubicuo: Documenta términos y conceptos
clave en un glosario.
- Modela el dominio: Identifica agregados, entidades y
value objects.
- Diseña para el comportamiento: Coloca reglas de negocio dentro
de las entidades y agregados.
- Utiliza arquitectura hexagonal: Separa claramente el dominio de
la infraestructura.
- Implementa persistencia con
repositorios:
Define interfaces en el dominio.
- Utiliza eventos de dominio: Para comunicación desacoplada
entre agregados.
- Mantén los agregados pequeños: Cada agregado debe tener una sola
responsabilidad.
- Refina continuamente: DDD es un proceso iterativo que
mejora con el aprendizaje.
Anexo
Pasando a
código:
// -- ENTIDADES Y VALUE OBJECTS --
// Value Object: CursoId
public class CursoId {
private
final UUID valor;
public
CursoId(UUID valor) {
this.valor
= Objects.requireNonNull(valor, "El
ID no puede ser nulo");
}
public
static CursoId generar() {
return new CursoId(UUID.randomUUID());
}
@Override
public
boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CursoId cursoId = (CursoId) o;
return
valor.equals(cursoId.valor);
}
@Override
public
int
hashCode() {
return Objects.hash(valor);
}
}
// Value Object: Precio
public class Precio {
private
final BigDecimal monto;
private
final String moneda;
public
Precio(BigDecimal monto, String moneda) {
if (monto.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("El
precio no puede ser negativo");
}
this.monto
= monto;
this.moneda
= Objects.requireNonNull(moneda, "La
moneda no puede ser nula");
}
public
Precio aplicarDescuento(BigDecimal
porcentaje) {
if
(porcentaje.compareTo(BigDecimal.ZERO) < 0 || porcentaje.compareTo(new
BigDecimal("100")) >
0) {
throw
new IllegalArgumentException("El
porcentaje debe estar entre 0 y 100");
}
BigDecimal factor = BigDecimal.ONE.subtract(porcentaje.divide(new BigDecimal("100")));
BigDecimal nuevoMonto = this.monto.multiply(factor).setScale(2, RoundingMode.HALF_UP);
return new Precio(nuevoMonto, this.moneda);
}
public
BigDecimal getMonto() {
return monto;
}
public
String getMoneda() {
return moneda;
}
}
// Entidad: Curso (Agregado Root)
public class Curso {
private
final CursoId id;
private
String titulo;
private
String descripcion;
private
int
capacidadMaxima;
private
Set<Inscripcion> inscripciones;
public
Curso(CursoId id, String titulo, String descripcion, int
capacidadMaxima) {
this.id = Objects.requireNonNull(id, "El
ID no puede ser nulo");
this.titulo = Objects.requireNonNull(titulo, "El
título no puede ser nulo");
this.descripcion
= Objects.requireNonNull(descripcion, "La
descripción no puede ser nula");
if
(capacidadMaxima <= 0) {
throw
new IllegalArgumentException("La
capacidad máxima debe ser mayor que cero");
}
this.capacidadMaxima
= capacidadMaxima;
this.inscripciones
= new HashSet<>();
}
public
static Curso crear(String titulo, String descripcion, int
capacidadMaxima) {
return
new Curso(CursoId.generar(), titulo, descripcion, capacidadMaxima);
}
public
Inscripcion inscribirEstudiante(Estudiante
estudiante, Precio precio) {
if
(inscripciones.size() >= capacidadMaxima) {
throw
new DominioExcepcion("El
curso ha alcanzado su capacidad máxima");
}
//
Verificar si el estudiante ya está inscrito
boolean
yaInscrito = inscripciones.stream()
.anyMatch(i
-> i.getEstudianteId().equals(estudiante.getId())
&&
!i.getEstado().equals(EstadoInscripcion.CANCELADA));
if
(yaInscrito) {
throw
new DominioExcepcion("El
estudiante ya está inscrito en este curso");
}
Inscripcion
inscripcion = Inscripcion.crear(this.id,
estudiante.getId(), precio);
inscripciones.add(inscripcion);
//
Publicar evento de dominio
EventBus.publicar(new
EstudianteInscritoEvent(inscripcion.getId()));
return
inscripcion;
}
public
void cancelarInscripcion(InscripcionId
inscripcionId) {
Inscripcion
inscripcion = buscarInscripcion(inscripcionId);
inscripcion.cancelar();
//
Publicar evento de dominio
EventBus.publicar(new
InscripcionCanceladaEvent(inscripcionId));
}
private
Inscripcion buscarInscripcion(InscripcionId
inscripcionId) {
return
inscripciones.stream()
.filter(i -> i.getId().equals(inscripcionId))
.findFirst()
.orElseThrow(()
-> new DominioExcepcion("Inscripción
no encontrada"));
}
//
Getters y otros métodos
}
// -- REPOSITORIOS (INTERFACES) --
public interface CursoRepository {
void
guardar(Curso curso);
Optional<Curso>
buscarPorId(CursoId
id);
List<Curso>
buscarPorTitulo(String
titulo);
}
// -- SERVICIOS DE DOMINIO --
public class ServicioInscripcion {
private
final CursoRepository cursoRepository;
private
final EstudianteRepository estudianteRepository;
public
ServicioInscripcion(CursoRepository
cursoRepository, EstudianteRepository estudianteRepository) {
this.cursoRepository
= cursoRepository;
this.estudianteRepository
= estudianteRepository;
}
public
Inscripcion inscribirEstudianteEnCurso(CursoId
cursoId, EstudianteId estudianteId, Precio
precio) {
Curso
curso = cursoRepository.buscarPorId(cursoId)
.orElseThrow(()
-> new RecursoNoEncontradoExcepcion("Curso
no encontrado"));
Estudiante
estudiante = estudianteRepository.buscarPorId(estudianteId)
.orElseThrow(()
-> new RecursoNoEncontradoExcepcion("Estudiante
no encontrado"));
Inscripcion
inscripcion = curso.inscribirEstudiante(estudiante, precio);
cursoRepository.guardar(curso);
return
inscripcion;
}
}
// -- IMPLEMENTACIÓN DEL REPOSITORIO (INFRAESTRUCTURA) --
public class CursoRepositoryImpl implements
CursoRepository {
private
final EntityManager entityManager;
public
CursoRepositoryImpl(EntityManager entityManager) {
this.entityManager =
entityManager;
}
@Override
public
void guardar(Curso curso) {
entityManager.persist(curso);
}
@Override
public
Optional<Curso> buscarPorId(CursoId
id) {
Curso
curso = entityManager.find(Curso.class,
id);
return
Optional.ofNullable(curso);
}
@Override
public
List<Curso>
buscarPorTitulo(String titulo) {
return entityManager.createQuery(
"SELECT
c FROM Curso c WHERE LOWER(c.titulo) LIKE :titulo", Curso.class)
.setParameter("titulo",
"%" +
titulo.toLowerCase() +
"%")
.getResultList();
}
}
// -- CAPA DE APLICACIÓN (CASOS DE USO) --
public class InscribirEstudianteUseCase {
private
final ServicioInscripcion servicioInscripcion;
public
InscribirEstudianteUseCase(ServicioInscripcion
servicioInscripcion) {
this.servicioInscripcion
= servicioInscripcion;
}
public
InscripcionDTO ejecutar(InscripcionCommand
command) {
CursoId
cursoId = new CursoId(command.getCursoId());
EstudianteId
estudianteId = new EstudianteId(command.getEstudianteId());
Precio
precio = new Precio(command.getMonto(), command.getMoneda());
Inscripcion
inscripcion = servicioInscripcion.inscribirEstudianteEnCurso(
cursoId, estudianteId,
precio);
return
new InscripcionDTO(
inscripcion.getId().getValor(),
inscripcion.getCursoId().getValor(),
inscripcion.getEstudianteId().getValor(),
inscripcion.getFechaInscripcion(),
inscripcion.getEstado().toString(),
inscripcion.getPrecio().getMonto(),
inscripcion.getPrecio().getMoneda()
);
}
}
// -- CONTROLADOR (ADAPTADOR PRIMARIO) --
@RestController
@RequestMapping("/api/inscripciones")
public class InscripcionController {
private
final InscribirEstudianteUseCase inscribirEstudianteUseCase;
public
InscripcionController(InscribirEstudianteUseCase
inscribirEstudianteUseCase) {
this.inscribirEstudianteUseCase
= inscribirEstudianteUseCase;
}
@PostMapping
public
ResponseEntity<InscripcionDTO> inscribirEstudiante(@RequestBody InscripcionCommand command) {
try {
InscripcionDTO inscripcionDTO =
inscribirEstudianteUseCase.ejecutar(command);
return
ResponseEntity.status(HttpStatus.CREATED).body(inscripcionDTO);
} catch (RecursoNoEncontradoExcepcion e) {
return ResponseEntity.notFound().build();
} catch (DominioExcepcion e) {
return ResponseEntity.badRequest().build();
}
}
}
En Resumen
Jorge Mercado
#JMCoach
No hay comentarios.:
Publicar un comentario
Nota: sólo los miembros de este blog pueden publicar comentarios.