Modelar el dominio
En el paso anterior el arquetipo te genero un Task minimo — un esqueleto con lo justo para que compile. Antes de usarlo en un caso de uso, vamos a evolucionarlo hacia una forma que refleje las convenciones del arquetipo y DDD pragmatico.
Leer el scaffold
Abri task/domain/Task.java. Esto es lo que el arquetipo dejo:
@Accessors(chain = true)
@Getter
@Entity
@Table(name = "tasks")
public class Task extends AlphaIdEntity {
@Transient
private Set<DomainEvent<?>> domainEvents = new LinkedHashSet<>();
public Task() {
domainEvents.add(TaskCreatedEvent.of(this));
}
}
Tres cosas ya estan resueltas:
-
PK alfanumerica — hereda de
AlphaIdEntity, asi que el ID se genera automaticamente como string corto no enumerable. No declaras@Idni@GeneratedValue. Para el detalle del mecanismo, ver Clases base JPA. -
Auditoria —
createdBy,createdDate,lastModifiedBy,lastModifiedDatevienen en el padre. -
Evento de dominio — el constructor publica
TaskCreatedEvent, encolado en una coleccion@Transient(no se persiste). Veremos como se procesa en una seccion futura sobre arquitectura event-driven.
Lo que el scaffold no resuelve:
-
No hay ningun campo del dominio — un
Tasksin titulo no tiene sentido. -
public Task()permite construir unTaskvacio desde cualquier lugar, sin forma de garantizar invariantes.
(El @Accessors(chain = true) se queda: no genera setters por si mismo — requiere @Setter — y firma el estilo fluent que preferimos en el arquetipo, como en persistencePort.save(Task.create(…)).)
Evolucion 1: agregar el titulo y un factory method
Agregamos title como campo obligatorio y reemplazamos el constructor publico por un factory method:
@Accessors(chain = true)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "tasks")
public class Task extends AlphaIdEntity {
private String title;
@Transient
private Set<DomainEvent<?>> domainEvents = new LinkedHashSet<>();
private Task(String title) {
this.title = title;
domainEvents.add(TaskCreatedEvent.of(this));
}
public static Task create(String title) {
return new Task(title);
}
}
Tres decisiones y su por que:
-
Factory method
Task.create(title)como unica puerta de creacion. El nombre expresa intencion ("crear una tarea"), y al centralizar la construccion, las invariantes viven en un solo lugar. -
Constructor privado con los campos requeridos. Si manana agregas
deadlinecomo obligatorio, lo agregas al constructor y el compilador te obliga a actualizar cada llamada. Un constructor que acepta lo minimo para dejar el objeto en estado valido. -
@NoArgsConstructor(access = PROTECTED). JPA necesita poder instanciar el objeto sin argumentos para rehidratarlo desde la base —PROTECTEDse lo permite sin exponerlo al codigo de dominio.
Evolucion 2: agregar el tipo de issue
Un Task tambien tiene un tipo (bug, feature, chore). Como es un conjunto cerrado de valores sin identidad propia, se modela como un enum de Java. Cream`lo en task/domain/IssueType.java:
package io.zutun.tutorial.task.domain;
public enum IssueType {
BUG, FEATURE, CHORE
}
Y lo agregamos al Task como campo obligatorio:
@Accessors(chain = true)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "tasks")
public class Task extends AlphaIdEntity {
private String title;
@Enumerated(EnumType.STRING)
private IssueType type;
@Transient
private Set<DomainEvent<?>> domainEvents = new LinkedHashSet<>();
private Task(String title, IssueType type) {
this.title = title;
this.type = type;
domainEvents.add(TaskCreatedEvent.of(this));
}
public static Task create(String title, IssueType type) {
return new Task(title, type);
}
}
@Enumerated(EnumType.STRING) le indica a JPA que persista el nombre del enum ("BUG"), no su ordinal. Esto hace la tabla legible y tolerante a reordenamientos del enum.
Que queda fuera de este paso
El modelo real de un gestor de tareas suele incluir mas — un Assignee como value object, Comment como entidad interna del aggregate, tal vez Priority. Esas piezas se cubren en Como definir el modelo de dominio, con el patron completo. Aca mantenemos el foco en evolucionar el scaffold sin sobrecargar el tutorial.
La persistencia del nuevo campo title en la base de datos (migracion Liquibase) se cubre en el paso de persistencia.
Siguiente paso
Ahora que Task expresa el dominio que queremos, en el siguiente paso vas a crear el caso de uso CreateTaskUseCase que construye tareas invocando Task.create(…).