AntonioGanci

Il blog di Antonio Ganci
posts - 201, comments - 420, trackbacks - 31

Configurare una macchina a stati usando una fluent interface

Devo implementare la seguente macchina a stati:

Trade state machine
L'immagine è stata realizzata tramite Graphviz

In pratica è una macchina a stati (semplificata) per immettere un ordine di acquisto e successivamente di vendita, per una serie di azioni, con lo scopo di ricavarne un utile.

Ad esempio supponiamo che vogliamo comprare 1.000 azioni dell'ENI al prezzo di 20 euro e rivenderle, sulla base di una nostra analisi, a 20,5 euro ricavandone un gain di 0,5 euro per azione. Il trade in questo caso avrà un ordine di acquisto (entry order) di 1.000 ENI a 20 euro (stato Entry: NotSubmitted) quando l'ordine di acquisto delle azioni, verrà ricevuto dalla banca (broker nel dominio) allora ci sarà il passaggio di stato a (Entry: Submitted) e così via.

Ho provato ad usare un approccio decisamente top down. Mi sono chiesto: Come vorrei che fosse il codice per configurare la macchina a stati? Ho, successivamente, cercato di implementarlo.

Ecco il risultato:

        private static TradeStateMachine CreateTradeStateMachine(Broker broker)
        {
            return new TradeStateMachineConfigurator(new TradeStateMachine())
                .State("EntryNotSubmitted")
                    .Action((trade) => broker.PlaceOrder(trade))
                        .OnSuccessGoTo("EntrySubmitted")
                        .OnFinecoExceptionGoTo("EntryError")
                .State("EntrySubmitted")
                    .Action((trade) => broker.IsExpired(trade))
                        .OnReturnValue_GoTo(true, "EntryNotExecuted")
                    .Action((trade) => broker.GetOrderStatus(trade))
                        .OnReturnValue_GoTo(OrderStatus.Submitted, "EntrySubmitted")
                        .OnReturnValue_GoTo(OrderStatus.Executed, "ExitNotSubmitted")
                        .OnFinecoExceptionGoTo("EntryError")
                .State("ExitNotSubmitted")
                .StateMachine();
        }

Come linea guida ho preferito avere il minor rumore possibile, ad esempio chiamando il metodo per aggiungere uno stato State anzichè AddState. Se avessi usato il verbo Add tutti i metodi si sarebbero chiamati Add, un'informazione secondo me inutile.

I metodi che vengono chiamati si riferiscono sempre all'ultimo oggetto creato, ad esempio:

                    .Action((trade) => broker.PlaceOrder(trade))

Si riferisce allo stato  EntryNotSubmitted, mentre:

                        .OnFinecoExceptionGoTo("EntryError")

si riferisce alla freccia che nella figura va da Entry: not submitted a Entry: error. I tab servono per rendere evidente in modo visivo questo concetto.

Se guardiamo le Action per lo stato EntrySubmitted vediamo che sono due diverse.
Qui sorge un problema: Quando eseguire la seconda azione definita?
Ho scelto di farlo quando l'esecuzione del codice della prima Action restituisce un risultato non gestito. In questo caso false.

L'ultima chiamata StateMachine() restituisce la state machine, passata nel construttore, pronta per essere eseguita.

Vediamo come implementare il codice per la classe TradeStateMachineConfigurator:

    class TradeStateMachineConfigurator
    {
        private readonly TradeStateMachine m_stateMachine;
 
        public TradeStateMachineConfigurator(TradeStateMachine stateMachine)
        {
            m_stateMachine = stateMachine;
        }
        public TradeStateMachineConfigurator State(string stateName)
        {
            m_stateMachine.Add(new TradeState(stateName));
            return this;
        }
        public TradeStateMachineConfigurator Action(Func<Trade, object> action)
        {
            m_stateMachine.CurrentState.Action(action);
            return this;
        }
        public TradeStateMachineConfigurator OnSuccessGoTo(string stateName)
        {
            m_stateMachine.CurrentState.CurrentAction.OnSuccessGoTo(stateName);
            return this;
        }
        public TradeStateMachineConfigurator OnFinecoExceptionGoTo(string stateName)
        {
            m_stateMachine.CurrentState.CurrentAction.OnFinecoExceptionGoTo(stateName);
            return this;
        }
        public TradeStateMachineConfigurator OnReturnValue_GoTo(object value, string stateName)
        {
            m_stateMachine.CurrentState.CurrentAction.OnReturnValue_GoTo(value, stateName);
            return this;
        }
        public TradeStateMachine StateMachine()
        {
            return m_stateMachine;
        }
    }

Tutti i metodi ritornano la stessa istanza di TradeStateMachineConfigurator, questo permette di effettuare chiamate multiple senza usare una variabile locale.

La property CurrentState restituisce l'ultimo stato aggiunto, in modo che le chiamate successive a State si riferiscano all'ultimo stato aggiunto. Discorso analogo per la property CurrentAction.

Le action sono delle lambda expression; nel nostro caso funzioni che hanno come parametro di input il Trade e un valore di ritorno.

L'implementazione della state machine è piuttosto semplice:

    class TradeStateMachine
    {
        private readonly List<TradeState> m_states = new List<TradeState>();
 
        public TradeState CurrentState { get { return m_states[m_states.Count - 1]; } }
 
        public void Add(TradeState tradeState) { m_states.Add(tradeState); }
        public void Start(Trade trade)
        {
            string nextState = "";
            TradeState current = m_states[0];
            do
            {
                nextState = current.Execute(trade);
                current = GetState(nextState);
            }
            while (!string.IsNullOrEmpty(nextState));
        }
        private TradeState GetState(IEquatable<string> stateName)
        {
            return m_states.Find((state) => stateName.Equals(state.Name));
        }
    }

L'esecuzione della macchina a stati termina quando il metodo Execute non ritorna il nome del prossimo stato.

Il TradeState è una collection di Action:

    class TradeState
    {
        private readonly string m_stateName;
        private readonly List<TradeStateAction> m_actions = new List<TradeStateAction>();
 
        public TradeState(string stateName) { m_stateName = stateName; }
 
        public TradeStateAction CurrentAction { get { return m_actions[m_actions.Count - 1]; } }
        public string Name { get { return m_stateName; } }
 
        public void AddAction(Func<Trade, object> action)
        {
            m_actions.Add(new TradeStateAction(action));
        }
        public string Execute(Trade trade)
        {
            foreach (TradeStateAction action in m_actions)
            {
                string stateName = action.Execute(trade);
                if (!string.IsNullOrEmpty(stateName))
                {
                    return stateName;
                }
            }
            return "";
        }
    }

Smette di eseguire le action nel momento in cui una di queste ritorna il nome del prossimo stato oppure ritorna la stringa vuota in questo caso cessa l'esecuzione della macchina a stati (stato finale).

Infine la classe TradeStateAction:

    class TradeStateAction
    {
        private readonly Func<Trade, object> m_action;
        private string m_finecoExceptionStateName;
        private readonly Dictionary<object, string> m_states = new Dictionary<object, string>();
        private string m_successStateName;
 
        public TradeStateAction(Func<Trade, object> action) { m_action = action; }
 
        public void OnFinecoExceptionGoTo(string stateName) { m_finecoExceptionStateName = stateName; }
        public void OnReturnValue_GoTo(object status, string stateName) { m_states.Add(status, stateName); }
        public void OnSuccessGoTo(string stateName) { m_successStateName = stateName; }
        public string Execute(Trade trade)
        {
            try
            {
                object returnValue = m_action.Invoke(trade);
                if (!string.IsNullOrEmpty(m_successStateName))
                {
                    return m_successStateName;
                }
                if (m_states.ContainsKey(returnValue))
                {
                    return m_states[returnValue];
                }
                return "";
            }
            catch (FinecoException)
            {
                if (string.IsNullOrEmpty(m_finecoExceptionStateName))
                {
                    throw;
                }
                return m_finecoExceptionStateName;
            }
        }
    }

Il metodo Execute ritorna il nome dello stato in base alle condizioni specificate. Viene data priorità nell'ordine a: OnFinecoException, OnSuccess, OnReturnValue.

Il codice l'ho scritto al volo per il post quindi potrebbe esserci qualche errore o mancanza, quello su cui vorrei porre l'attenzione è la leggibilità del codice di configurazione della state machine.

Lo trovate leggibile? Suggerimenti per migliorarlo? Il design rivela le intenzioni?

Print | posted on domenica 11 gennaio 2009 16:27 |

Feedback

Gravatar

# re: Configurare una macchina a stati usando una fluent interface

Bellissima soluzione,veramente pulita ed elegante. Un evoluzione sarebbe implementare un DSL Tool dato il diagramma generi il codice.
11/01/2009 17:08 | Giuseppe Lippolis
Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET