====== 04.1 - Modelos y servicios de dominio ====== La capa ''domain'' contiene las reglas y conceptos propios del negocio. Debe expresarlos sin depender de mecanismos de presentación o persistencia. ===== 1. Modelos anémicos y enriquecidos ===== Un **modelo anémico** representa datos y deja las reglas y operaciones en otros componentes. Un **modelo enriquecido** combina estado y comportamiento: protege sus invariantes y ofrece operaciones que expresan acciones del negocio. El mismo concepto de pedido puede expresarse como datos sin comportamiento o como un objeto que protege sus propias reglas: // Modelo anémico: estructura de datos public record OrderData(long id, OrderStatus status) {} // Modelo enriquecido: estado y comportamiento public class Order { private OrderStatus status; public void cancel() { if (status != OrderStatus.PENDING) { throw new OrderCannotBeCancelledException(); } status = OrderStatus.CANCELLED; } } ^ **Estilo** ^ **Ventajas** ^ **Costes y límites** ^ | **Anémico** | Estructura sencilla; puede compartirse como datos entre componentes | El comportamiento queda fuera del objeto; compartir el mismo tipo entre capas acopla sus contratos y facilita que las reglas se dispersen o dupliquen | | **Enriquecido** | Reglas e invariantes junto al estado que protegen; aprovecha la encapsulación de la **POO** | Para separar el modelo interno de los contratos externos se necesitan DTO y mapeos; un cambio puede afectar a varias representaciones | Los estilos pueden combinarse. La elección depende de la complejidad del dominio y de cómo mantener explícitos los límites sin duplicar reglas. ===== 2. Servicios de dominio ===== Un **servicio de dominio** expresa una operación que combina varios modelos o una regla que no pertenece naturalmente a uno solo. Si una regla protege la invariante de un modelo concreto, debe vivir en ese modelo. Con modelos anémicos, las operaciones que combinan datos y reglas deben residir fuera de ellos. Los **servicios** reúnen ese comportamiento; si hay varios flujos de aplicación, los **casos de uso** pueden coordinarlos y reutilizar esos servicios sin duplicar el flujo. No es una obligación derivada del estilo: se separan cuando hay una responsabilidad de aplicación que coordinar. Con modelos enriquecidos, las reglas que protegen el estado de un objeto pueden vivir en el propio modelo. Los servicios siguen siendo útiles para operaciones que involucran varios modelos o políticas que no pertenecen a uno solo. Cuando un contrato de servicio cruza límites entre capas o módulos, puede expresarse con **DTO** para transportar únicamente los datos necesarios, sin exponer directamente los modelos enriquecidos. En **Java**, los ''record'' permiten declarar estos contratos de forma concisa: public record CancelOrderDto(long orderId) {} public record OrderSummaryDto(long id, OrderStatus status) {} public interface OrderService { OrderSummaryDto cancel(CancelOrderDto request); } public class OrderServiceImpl implements OrderService { private final OrderRepository orders; private final OrderMapper mapper; public OrderServiceImpl(OrderRepository orders, OrderMapper mapper) { this.orders = orders; this.mapper = mapper; } @Override public OrderSummaryDto cancel(CancelOrderDto request) { Order order = orders.findById(request.orderId()) .orElseThrow(OrderNotFoundException::new); order.cancel(); return mapper.toSummary(order); } } El servicio recibe y devuelve DTO; cuando necesita convertir entre el modelo enriquecido y el formato de salida, **MapStruct** puede generar el código repetitivo del mapeo: @Mapper public interface OrderMapper { OrderSummaryDto toSummary(Order order); } El mapeador transforma datos; no aplica reglas de negocio. Su construcción e inyección se resuelven en la composición de la aplicación, sin añadir dependencias de Spring al dominio. Un servicio puede colaborar con otros servicios de su módulo. Si el servicio requerido pertenece a otro módulo, la llamada se realiza a través de la **API pública** de ese módulo y su contrato DTO. La colaboración corresponde al servicio cuando es parte estable de su comportamiento; si depende de un flujo concreto, la coordina el caso de uso. Esta distinción se desarrolla en el tema 04.2. ===== 3. Repositorios como abstracción ===== El **repositorio** expresa qué operaciones necesita el dominio para recuperar y guardar modelos. La interfaz se define en ''domain'' y su implementación de acceso a datos queda fuera, en la capa de persistencia. La interfaz declara las operaciones desde la perspectiva del dominio, usando el modelo enriquecido: public interface OrderRepository { Optional findById(long id); void save(Order order); } @startuml allowmixing package "domain" { interface OrderRepository class OrderServiceImpl class Order } package "persistence" { class OrderRepositoryImpl } database "Base de datos" as db OrderServiceImpl --> OrderRepository : utiliza OrderServiceImpl --> Order : aplica reglas OrderRepositoryImpl ..|> OrderRepository : implementa OrderRepositoryImpl --> db @enduml Esta separación aplica **inversión de dependencias**: el dominio declara la abstracción que necesita; la persistencia la implementa. ===== 4. Pruebas de servicios y modelos ===== * Probar las reglas e invariantes del modelo directamente, sin framework ni base de datos. * Probar los servicios con dependencias de persistencia aisladas mediante **mocks** o **fakes**. * Verificar resultados, excepciones y colaboración relevante; evitar comprobar detalles internos de implementación. Una prueba de servicio debe comprobar el comportamiento del dominio, no volver a probar la persistencia. La implementación real del repositorio y su integración con el almacenamiento se comprueban en pruebas de persistencia. @ExtendWith(MockitoExtension.class) class OrderServiceImplTest { @Mock OrderRepository orders; @Mock OrderMapper mapper; @InjectMocks OrderServiceImpl service; @Test void cancelsPendingOrder() { Order order = new Order(OrderStatus.PENDING); OrderSummaryDto summary = new OrderSummaryDto(42L, OrderStatus.CANCELLED); when(orders.findById(42L)).thenReturn(Optional.of(order)); when(mapper.toSummary(order)).thenReturn(summary); OrderSummaryDto result = service.cancel(new CancelOrderDto(42L)); assertEquals(OrderStatus.CANCELLED, order.getStatus()); assertEquals(summary, result); verify(orders).findById(42L); verify(mapper).toSummary(order); } } El repositorio es un **mock**: la prueba verifica el servicio y la regla del modelo sin iniciar una base de datos. Las operaciones reales de almacenamiento se comprueban en pruebas de persistencia. Los modelos ricos pueden probarse como objetos ordinarios. Aísla las dependencias externas de los servicios y reserva las pruebas de base de datos para la capa de persistencia.