martes, 3 de junio de 2025

Resumen y ejemplo DDD

 

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

  1. Lenguaje Ubicuo: Vocabulario común compartido entre desarrolladores y expertos del dominio.
  2. Modelo de Dominio: Representación abstracta del dominio del negocio y sus reglas.
  3. Contextos Delimitados: Fronteras explícitas donde un modelo particular es válido.
  4. Entidades, Value Objects, Agregados: Bloques de construcción del modelo.
  5. Servicios de Dominio: Operaciones que no pertenecen naturalmente a entidades o value objects.
  6. Eventos de Dominio: Notificaciones de cambios significativos en el dominio.
  7. Repositorios: Abstracción para persistencia de agregados.

Cómo implementar DDD con éxito

  1. 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)
  2. 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
  3. Arquitectura hexagonal:
    • Separa el dominio de la infraestructura
    • Implementa puertos y adaptadores
    • Usa inyección de dependencias
  4. 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
  5. 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:

  1. Value Objects inmutables (CursoId, Precio)
  2. Entidades con identidad y comportamiento (Curso)
  3. Reglas de negocio encapsuladas dentro del dominio
  4. Invariantes protegidos con validaciones
  5. Interfaces de repositorio definidas en el dominio
  6. Servicios de dominio para orquestar operaciones entre agregados
  7. Eventos de dominio para comunicación entre agregados
  8. Capas de aplicación que implementan casos de uso
  9. Adaptadores que conectan con el mundo exterior

Pasos para implementar DDD con éxito

  1. Comienza con Event Storming: Reúne expertos en dominio y técnicos para mapear eventos clave.
  2. Define contextos delimitados: Identifica los límites naturales en tu dominio.
  3. Construye el lenguaje ubicuo: Documenta términos y conceptos clave en un glosario.
  4. Modela el dominio: Identifica agregados, entidades y value objects.
  5. Diseña para el comportamiento: Coloca reglas de negocio dentro de las entidades y agregados.
  6. Utiliza arquitectura hexagonal: Separa claramente el dominio de la infraestructura.
  7. Implementa persistencia con repositorios: Define interfaces en el dominio.
  8. Utiliza eventos de dominio: Para comunicación desacoplada entre agregados.
  9. Mantén los agregados pequeños: Cada agregado debe tener una sola responsabilidad.
  10. 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.

Tecnología con propósito, trabajos en piloto.

Transformando México con IA, IoT y Apps Inteligentes: 3 Casos de Uso con Impacto Real en fase de prueba piloto. L a tecnología no solo debe...