Spring Statemachine

Qualche tempo fa, nell’articolo Automi e Gestione dello Stato, abbiamo parlato di un framework per l’implementazione di una macchina a stati finiti, ovvero di automi che possono rivelarsi molto utili quando, in un progetto, si ha la necessità di dover gestire lo stato di un oggetto di business delle possibili azioni applicabili ad esso, in base allo stato, e delle conseguenti transizioni allo stato successivo.

L’importanza dell’impiego di tali automi in sistemi reali, è maggiormente avvalorata dal fatto che un progetto open source rilevante come Spring, ha recentemente rilasciato un suo framework per la gestione di macchine a stati: Spring Statemachine. Per comprendere la robustezza e l’usabilità di tale framework, proveremo ad implementare il semplice processo di purchase order, già introdotto nell’articolo Automi e Gestione dello Stato, e che riportiamo per comodità nella figura seguente.

Naturalmente anche Spring Statemachine supporta il disegno del processo mediante un file descrittore, che in particolare, è un diagramma UML disegnato mediante l’ambiente di modellazione UML Papyrus per Eclipse. Per il nostro esempio però utilizzeremo un approccio programmatico.

Le Dipendenze

Per l’utilizzo del framework è necessaria una unica dipendenza spring-statemachine-core, anche se esistono diversi altri moduli che gli consentono, ad esempio, di essere facilmente integrato con Spring Boot o di supportare l’accesso a database relazionali o NoSQL, o ancora di essere utilizzato con Spring Cloud, etc.

Alla dipendenza core di Statemachine aggiungiamo inoltre quelle necessarie all’utilizzo di a Spring JavaConfig e Spring core:

Eventi e Stati

Gli stati che caratterizzano una macchina a stati finiti di Spring e gli eventi che determinano le transizioni da uno stato al successivo, possono essere rappresentati indifferentemente da classi String o Enum. In generale se si vuole caratterizzare tali elementi con informazioni aggiuntive è preferibile utilizzare gli Enum, che essendo fondamentalmente delle classi java, possono essere arricchite con ulteriori proprietà.  Per l’esempio del PO abbiamo quindi utilizzato due classi Enum così definite.

Definizione dell’Automa

Il metodo scelto da Spring per definire una macchina a stati è quello di utilizzare una classe di configurazione di JavaConfig, ovvero una classe annotata con l’annotazione @Configuration. Spring Statemachine introduce due ulteriori annotazioni con cui annotare la classe di configurazione,  che abilitano le funzioni necessarie a supportare una macchina a stati finiti. Tali annotazioni sono @EnableStateMachine@EnableStateMachineFactory.

La differenza tra le due annotazioni è che la prima genera una istanza della macchina a stati all’avvio dell’applicazione, che resta disponibile nel contesto di Spring (come singleton) per tutta la sua vita. La seconda invece configura l’automa ma la sua generazione è differita al momento in cui il programmatore, con una specifica classe factory, deciderà di istanziarla. Conseguentemente se si intende gestire con una macchina a stati l’evoluzione di un oggetto di business, come un ordine di acquisto, che può essere istanziato più volte nell’applicazione, deve essere utilizzata l’annotazione @EnableStateMachineFactory.

Il framework mette inoltre a disposizione due classi adapter EnumStateMachineConfigurerAdapter e StateMachineConfigurerAdapter che possono essere estese e che definiscono i metodi di callback per la configurazione della macchina a stati. In particolare devono essere sovrascritti due metodi necessari per la configurazione degli stati e delle transizioni ammesse in base agli eventi definiti:

Quella mostrata è la configurazione in assoluto più semplice di un automa, in cui tutti gli stati sono allo stesso livello, e che bene si adatta al nostro problema dell’ordine di acquisto. In generale però il framework è in grado di gestire stati gerarchici, ovvero in cui ad uno stato corrispondono sotto-stati differenti. Conseguentemente anche le transizioni possono essere classificate in modo diverso in funzione del fatto che possano avere un effetto sui soli sotto-stati o anche sugli stati gerarchicamente superiori.

Action

Alle transizioni e agli eventi di ingresso ed uscita da uno stato possono essere associate delle azioni che il motore esegue al verificarsi dell’evento. Tornando all’esempio del PO, in conseguenza delle transizioni approve e deny, è previsto che vengano valorizzate opportunamente le proprietà dell’ordine: number, approvalDate e cancelDate. A scopo nella definizione delle transazioni il framework mette a disposizione un metodo action() che riceve in ingresso l’azione da eseguire:

I due metodi approceAction() e denyAction() restituiscono un oggetto che implementa l’interfaccia  Action<OrderStates, OrderEvents>, la quale definisce un unico metodo execute() che è quello invocato dal framework quando la logica dell’action deve essere eseguita.

Prima di procedere con l’implementazione concreta dei due metodi dobbiamo introdurre l’interfaccia ExtendedState. Tale interfaccia è utilizzata per arricchire lo stato dell’automa con variabili (di stato) aggiuntive. Nel nostro caso, ad esempio, l’oggetto Order gestito dal processo PO è memorizzato nell’ExtendedState della macchina. L’accesso a tale stato “esteso” è possibile attraverso il metodo getExtendedState() della classe StateMachine o StateContext, a seconda dei casi.

Il metodo execute() dell’interfaccia Action prevede in input un oggetto di tipo StateContext, per cui l’implementazione dei metodi approceAction() e denyAction() sarà:

dove il metodo findOrder() ricerca l’ordine tra le variabili memorizzate nell’extended state:

Come detto inizialmente è anche possibile associare azioni ad uno stato, che vengono eseguite in corrispondenza degli eventi di ingresso ed uscita dallo stesso, indipendentemente dalla transizione che li hanno generati. Per completezza mostriamo un esempio di come eseguire tale associazione, fermo restando che l’implementazione dell’action segue la medesima logica. Aggiorniamo quindi la definizione degli stati nel seguente modo:

dove al metodo state() sono state aggiunte le azioni corrispondenti all’ingresso ed all’uscita dallo stato APPROVED, e che sono così definite:

Guard

Ultima feature di cui abbiamo bisogno per implementare il processo PO è l’interfaccia Guard, che è utilizzata per “proteggere” le transizioni tra gli stati. Tale interfaccia definisce un unico metodo evaluate(), che restituisce un boolean, e che è invocato prima della esecuzione della transizione a cui è associata. L’effetto è quello di bloccare la transazione nel caso in cui sia restituito il valore FALSE.

Nel caso del processo di PO, il vincolo che deve essere implementato è quello di non consentire l’approvazione di richieste con un budget maggiore di un importo prefissato (100$). A tale scopo alla transizione dallo stato CREATED a quello APPROVED è associato un oggetto Guard nel modo seguente:

dove il metodo budgetGuard() definisce una classe Guard che restituisce FALSE nel caso in cui il budget dell’ordine sia superiore al limite fissato:

Esecuzione della Macchina a Stati

L’interazione con la macchina a stati finiti che abbiamo definito per il processo di PO implica due aspetti fondamentali:

  1. generazione di una nuova istanza della macchina a stati per ogni richiesta di acquisto;
  2. interazione con la macchina a stati al fine di far procedere l’ordine, transitando tra i diversi stati previsti.

Il primo aspetto è coperto dal framework attraverso l’interfaccia StateMachineFactory ed il metodo getStateMachine() che genera e restituisce un’istanza della classe StateMachine che rappresenta la macchina a stati richiesta. Introduciamo quindi, per semplicità, la classe OrderManager per la gestione degli ordini di acquisto e definiamo il metodo newOrder() nel seguente modo:

In il metodo nell’ordine:

  1. istanzia la macchina associandogli un machine-id derivato dall’id dell’ordine;
  2. inserisce l’ordine nell’extended state della macchina (importante per poter essere recuperato dalle Action e Gurd definite);
  3. avvia la macchina che si “posizionerà” sullo stato iniziale di CREATED.

L’evoluzione della macchina tra i vari stati, che copre il secondo degli aspetti considerati, si realizza attraverso l’invio di eventi o segnali alla macchina. Tali eventi, che sono evidentemente quelli definiti in fase di configurazione, possono essere inviati attraverso il metodo sendEvent() della classe StateMachine. Procedendo con l’implementazione della classe OrderManager introduciamo il metodo generico fire() così definito:

Esempi di Richieste di Acquisto

Per testare il comportamento della macchina consideriamo due richieste di acquisto, una con budget superiore al limite ed una con budget inferiore. Creiamo quindi un ordine per un laptop di importo pari a 1000 euro e procediamo approvando l’ordine:

Come ci aspettiamo lo stato dell’ordine prima e dopo l’evento di approvazione rimane inalterato in CREATED.

Procediamo eseguendo lo stesso test questa volta per l’acquisto di un mouse del valore di 50 euro:

Questa volta la condizione imposta dalla condizione di Guard è superata e la macchina si porta nello stato APPROVED. Inoltre le proprietà number e approvalDate vengono valorizzate dalla Action associata alla transazione.

Per completezza terminiamo il flusso di approvazione inviando il segnale PROCESS che porta la macchina nello stato PROCESSED.  Si tratta di uno stato classificato come stato finale (end state) ed il framework ce lo segnala esponendo un metodo isCompleted() della classe StateMachine che, se invocato, restituirà TRUE.

L’output prodotto sarà infatti:

Codie Sorgente

Il codice sorgente del progetto è disponibile qui spring-statemachine.

 

How useful was this post?

Click on a star to rate it!

Average rating / 5. Vote count:

No votes so far! Be the first to rate this post.

As you found this post useful...

Follow us on social media!