E’ abbastanza comune in applicazioni enterprise la necessità di dover gestire lo stato degli oggetti di business. Si pensi ad esempio ad un documento contabile, una fattura o al classico ordine di acquisto. Oggetti che vengono inizialmente creati in stato di “bozza” e vi rimangono sino a quando l’utente non ne termina la lavorazione per renderlo disponibile ad una fase successiva di processamento.
La gestione degli stati e soprattutto delle transizioni tra uno stato ed il successivo può essere realizzato in differenti maniere. La soluzione più semplice e meno flessibile è quella di gestirla da programma, mentre la più complessa ma di facile manutenibilità è l’utilizzo di sistemi di Business Process Management (BPM) o di workflow come JBPM o Activiti.
L’utilizzo di tali sistema è spesso la soluzione che più spaventa e per questo frequentemente quello che accade è che si preferisce realizzare la gestione degli stati attraverso apposite routine di programma perché, erroneamente, si ritiene siano più semplici da manutenere. In questo post vogliamo presentare una soluzione intermedia, che implica l’utilizzo di un Automa a Stati Finiti. Una versione più semplice ed abbordabile di sistema di workflow, che consente comunque di descrivere in modo formale il comportamento atteso da una applicazione.
Il Processo
La soluzione che descriviamo nel post implica l’utilizzo del framework open source Automata i cui sorgenti sono disponibili su GitHub e le dipendenze su Nexus Sonotype. Come caso di studio consideriamo la gestione di un ordine di acquisto (PO o purchase order) descritto nel wiki del progetto Automata ed il cui processo di gestione è descritto dal flusso mostrato nell’immagine seguente:
Sostanzialmente il PO viene inizialmente creato e appena sottomesso viene automaticamente approvato o rifiutato in base al budget previsto. Solo in caso di approvazione un utente ha la possibilità di cancellarlo o processarlo.
Utilizzando tale processo realizzeremo una semplice web application per la gestione di una richiesta di acquisto e vedremo come, progettandolo opportunamente, sia possibile rendere il sistema robusto a modifiche minime del processo.
Descrizione dell’Automa
La descrizione del processo può essere fatta mediante l’utilizzo delle API del framework o più semplicemente mediante un XML. Il file XML che rappresenta il workflow purchase order descritto sopra ed utilizzato nel progetto è il seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<workflow name="purchaseOrder" start="CREATED" itemName="order"> <state name="CREATED" description="New order"> <transition to="APPROVED" name="approve" description="Approve order"> <condition> <![CDATA[ return order.budget < 1000; ]]> </condition> <action> order.number = (int)(Math.random()*100000); </action> </transition> <transition to="DENIED" name="deny" description="Deny order"> <condition> <![CDATA[ return order.budget >= 1000; ]]> </condition> <action> order.note +='Order denied'; </action> </transition> </state> <state name="APPROVED" description="Approved"> <transition to="CANCELLED" name="cancel" description="Calcel order"/> <transition to="PROCESSED" name="process" description="Process order"/> </state> <state name="DENIED" description="Denied"/> <state name="CANCELLED" description="Cancelled"/> <state name="PROCESSED" description="Processed"/> </workflow> |
La Web Application
Prima di entrare nei dettagli implementativi presentiamo le due maschere che compongono l’applicazione e le relative caratteristiche, in modo da rendere chiaro quanto descritto nel seguito. La prima maschera è quella di edit utilizzata per l’inserimento ma anche per la modifica o la semplice visualizzazione dell’ordine:
La seconda maschera visualizza la lista degli ordini gestiti:
Inizialmente tutti gli ordini sono in stato CREATED
(lo stato iniziale del processo). Alla pressione del pulsante Submit è avviata la prima transazione ovvero quella automatica verso gli stati APPROVED
o DENIED
. La situazione risultante per i tre ordini sarebbe:
Si nota che l’ordine 1 è i solo approvato in quanto gli altri superano il budget gestito dal processo. L’utente non può più operare sugli altri ordini se non visualizzarne i dettagli. Diversamente per l’ordine 1 può eseguire il cancel o i process. A seguito del processamento l’ordine raggiunge lo stato PROCESSED
ed il dettaglio appare come mostrato nell’immagine seguente, dove è anche visibili lo stato attuale e lo storico degli stati attraverso cui è passato l’ordine:
Il Progetto
Il progetto di esempio sarà implementato utilizzando Maven e JSF. Per il setup seguiamo quanto già descritto nel post Creare Web App con JSF 2.0. Quindi modifichiamo il pom introducendo la dipendenza al framework Automata:
1 2 3 4 5 |
<dependency> <groupId>it.inspiredsoft</groupId> <artifactId>automata</artifactId> <version>1.0.0</version> </dependency> |
Creiamo quindi gli oggetti Order
e OrderHistory
che implementano rispettivamente le interfacce del framework ExtendedWorkItem
e HistoryItem
. In particolare Order
ha la seguente struttura (differente da quella mostrata nell’esempio del framework):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public class Order implements ExtendedWorkItem<OrderHistory> { private Long id; private String item; // Item to order private Integer number; // Approval number private String note; // Note private BigDecimal budget; // Total paid for the order /* Inherited from ExtendedWorkItem */ private String state; // State of the order private Date stateTime; // Starting date of the state private List<OrderHistory> histories = new ArrayList<OrderHistory>(); // The history of the item public Order(){} public Order(String item){ this.item = item; } /* Enum for the states */ public enum States{ CREATED("CREATED"), APPROVED("APPROVED"), DENIED("DENIED"), PROCESSED("PROCESSED"), CANCELLED("CANCELLED"); States( String value ){ this.value = value; } private String value; public String getId( ){ return value; } } /* Getter and setter */ } |
A questo punto introduciamo i due managed bean utilizzati per la web application. Il primo è il WorkflowManagerBean
che estende la classe ConfigurableWorkflowManager
del framework per eseguire il caricamento dell’XML.
1 2 3 4 5 6 7 8 |
@ManagedBean @ApplicationScoped public class WorkflowManagerBean extends ConfigurableWorkflowManager { public WorkflowManagerBean() throws Exception { URL url = Thread.currentThread().getContextClassLoader().getResource( "purchase-order.fsm.xml" ); super.setPaths( Arrays.asList( url.getPath() ) ); } } |
Il secondo bean, OrderManagedBean
, implementa la logica necessaria alle due maschere XHTML
che visualizzano l’elenco degli ordini e il dettaglio dell’ordine. In particolare nel bean è iniettato il WorkflowManagerBean
ed un OrderService
che implementa in modo fake i metodi di accesso agli ordini persistiti.
Con riferimento alla maschera che elenca gli ordini mostriamo il codice XHTML che implementa i pulsanti ed i metodi Java presenti nel bean OrderManagedBean
attivati dalla loro pressione.
Submit
Il metodo java recupera la descrizione del processo ed esegue il submit. Secondo specifiche tale metodo del framework esegue la prima transizione possibile secondo le condizioni espresse nell’XML. Il processo segue quindi la transizione approve per ordini inferiori a 1000 e deny in caso contrario.
1 |
<h:commandButton action="#{orderManagedBean.submit(order)}" value="Submit" rendered="#{order.created}"/> |
1 2 3 4 5 |
public void submit( Order order ) { Workflow po = workflowManagerBean.getWorkflow("purchaseOrder"); WorkflowContext context = new WorkflowContext( order ); workflowManagerBean.submit( po, context ); } |
Cancel Order e Process Order
Questi pulsanti non sono codificati nella maschera ma sono il risultato del recupero delle transizioni possibili a partire dallo stato APPROVED. Le label dei pulsanti corrispondono alla descrizione della transazione:
1 2 3 |
<ui:repeat var="trans" value="#{orderManagedBean.getTransitions(order)}"> <h:commandButton action="#{orderManagedBean.fire(trans, order)}" value="#{trans.description}" /> </ui:repeat> |
1 2 3 4 5 |
public List<Transition> getTransitions(Order order) { Workflow po = workflowManagerBean.getWorkflow("purchaseOrder"); State state = po.getCurrentState( order ); return new ArrayList<Transition>( state.getTransitions() ); } |
Alla pressione di uno dei due pulsanti viene semplicemente richiesto al workflow di seguire la transazione associata:
1 2 3 4 |
public void fire(Transition transition, Order order) { WorkflowContext context = new WorkflowContext( order ); workflowManagerBean.fire( transition, context ); } |
Modifica del Processo
Al paragrafo precedente abbiamo visto come sia possibile avere una maschera che mostra dinamicamente i pulsanti in funzione dello stato dell’ordine. Vediamo ora come, modificando il processo, non sia necessario modificare la maschera e la logica già implementata per adattarsi alla nuova “specifica”.
A tale scopo inseriamo una nuova transazione rejected allo stato APPROVED
che riporta l’ordine in stato CREATED
.
1 2 3 4 5 |
<state name="APPROVED" description="Approved"> <transition to="CANCELLED" name="cancel" description="Calcel order"/> <transition to="PROCESSED" name="process" description="Process order"/> <transition to="CREATED" name="reject" description="Reject order"/> </state> |
Questa semplice operazione comporterà la presenza di un nuovo pulsante Reject order che sarà visualizzato nella maschera senza la necessità di alcun altro intervento:
Complichiamo ora l’esempio supponendo di voler consentire il reject solamente per ordini con importo superiori a 500. In questo caso dobbiamo modificare la transition nel modo seguente:
1 2 3 4 5 |
<transition to="CREATED" name="reject" description="Reject order"> <condition> return order.budget >= 500; </condition> </transition> |
Questo però non è sufficiente perché da specifica il metodo fire()
del framework esegue la transizione senza valutare eventuali condizioni. E’ necessario quindi non prospettare all’utente la transizione rejected. Per farlo ancora una volta il framework ci viene in aiuto col metodo evaluateCondition()
che utilizziamo nel metodo getTransitions()
di OrderManagedBean
per filtrare le transizioni consentite:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public List<Transition> getTransitions(Order order) { List<Transition> transitions = new ArrayList<Transition>(); Workflow po = workflowManagerBean.getWorkflow("purchaseOrder"); State state = po.getCurrentState( order ); WorkflowContext context = new WorkflowContext( order ); for ( Transition transition : state.getTransitions() ) { if ( workflowManagerBean.evaluateCondition( transition, context ) ) { transitions.add( transition ); } } return transitions; } |
Conclusione
Sostanzialmente il framework è fortemente orientato alla gestione dello stato di un oggetto di business mediante automi a stati finiti, fornendo tutto il supporto necessario ad automatizzare i cambi di stato e la loro storia. Per semplicità riepiloghiamo nella tabella seguente le caratteristiche principali:
WorkItem , ExtendedWorkItem e HistoryItem |
Sono le interfacce da estendere per implementare gli automatismi che consentono al framework di gestire lo stato degli oggetti e la loro history. |
submit() |
Il metodo valuta le condizioni associate a tutte le transazioni possibili a partire dallo stato corrente ed esegue la prima che restituisce TRUE. |
fire() |
Il metodo esegue la transazione specificata in input ignorando completamente eventuali condizioni associate. |
evaluateCondition() |
Valuta la condizione associata ad una transazione specificata in input. |
Codice Sorgente
Il codice sorgente completo per l’esempio descritto è scaricabile qui purchase-order.