Step 05 — Domain logic + state machine¶
Obiettivo: mettere le regole intrinseche dell'entità dentro l'entità (always-valid object).
Il problema¶
Il task ha 3 stati: TODO, IN_PROGRESS, DONE. Non tutte le transizioni sono valide:
stateDiagram-v2
[*] --> TODO : create
TODO --> IN_PROGRESS
TODO --> DONE
IN_PROGRESS --> DONE
IN_PROGRESS --> TODO : reopen
DONE --> TODO : reopen Dove vive la regola?
Tre opzioni:
| Posto | Pro | Contro |
|---|---|---|
| Controller | facile | la regola sparisce se chiami il service da altro contesto |
| Service | centrale | facile dimenticarsene in metodi nuovi |
| Dominio (entity) | sempre valida, anche da test e job | un po' più di codice |
Scegliamo il dominio.
Implementazione¶
TaskStatus.java
public enum TaskStatus {
TODO, IN_PROGRESS, DONE;
public boolean canTransitionTo(TaskStatus target) {
if (target == null || target == this) return false;
return ALLOWED.getOrDefault(this, Set.of()).contains(target);
}
private static final Map<TaskStatus, Set<TaskStatus>> ALLOWED = Map.of(
TODO, Set.of(IN_PROGRESS, DONE),
IN_PROGRESS, Set.of(TODO, DONE),
DONE, Set.of(TODO)
);
}
Task.java (estratto)
public void changeStatus(TaskStatus newStatus) {
if (!this.status.canTransitionTo(newStatus)) {
throw new InvalidStateTransitionException(this.status.name(), newStatus.name());
}
this.status = newStatus;
}
Concetti chiave¶
- Always-valid object — l'entità non può mai trovarsi in uno stato invalido.
- L'eccezione
InvalidStateTransitionExceptionviene tradotta in HTTP 409 dalGlobalExceptionHandler. - Stessa idea per
Project#archive/Project#reactivate(anche se più semplici).
Esercizi¶
- Aggiungi uno stato
CANCELLEDe definisci le transizioni. - Aggiungi un campo
closedAtsettato automaticamente quando si va aDONE. - Discuti come implementare la stessa regola con il pattern Spring Statemachine.