Vai al contenuto

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 InvalidStateTransitionException viene tradotta in HTTP 409 dal GlobalExceptionHandler.
  • Stessa idea per Project#archive / Project#reactivate (anche se più semplici).

Esercizi

  1. Aggiungi uno stato CANCELLED e definisci le transizioni.
  2. Aggiungi un campo closedAt settato automaticamente quando si va a DONE.
  3. Discuti come implementare la stessa regola con il pattern Spring Statemachine.

Riferimenti nel codice