State pattern nel mondo reale

Le macchine a stati sono argomento di studio per tutti coloro che seguono i corsi di informatica o ingegneria informatica. Spesso restano un argomento di studio perché appaiono lontane dalla realtà di tutti i giorni.

In realtà tutti i nostri programmi hanno uno stato interno che varia di continuo. Di solito però non abbiamo bisogno di formalizzarli come macchine a stati.

Ad esempio la seguente macchina a stati

Untitled Diagram(1)

potrebbe modellare questo programma:

void Main() {
    while (true) {
    string res = Console.ReadLine();
    Console.WriteLine(res);
    }
}

Implementare una macchina a stati per descrivere il comportamento del programma precedente sarebbe un’inutile complicazione e renderebbe il programma molto più lungo.

Però ci sono casi in cui una macchina a stati può venire in aiuto.
Ad esempio, nell’ultimo periodo ho dovuto modellare un sistema di approvvigionamento di una linea di assemblaggio che utilizza dei robot per portare la materia prima da alcuni magazzini automatici verso le macchine dell’assemblaggio.
Gli attori coinvolti sono:

  • le macchine assemblatrici
  • il sistema che gestisce i robot
  • i magazzini automatici

Le situazioni da controllare sono molte, ad esempio:

  • verificare se è richiesta una nuova cassetta dall’assemblaggio
  • comandare l’uscita di una cassetta da uno dei magazzini automatici
  • verificare se, per una certa macchina assemblatrice, una cassetta sta già uscendo
  • controllare se le uscite dei magazzini automatici sono libere o occupate e scegliere di conseguenza l’uscita dove convogliare la cassetta richiesta
  • comandare un trasferimento al sistema di gestione dei robot
  • verificare lo stato dei trasferimenti pendenti

In un caso come questo, data la complessità, è molto più facile utilizzare una macchina a stati che non utilizzarla.
Questa è la versione semplificata della macchina a stati che ho usato in questa situazione:

macchina_stati_ciclo_pieni

Per implementare via codice una macchina a stati ho utilizzato lo state pattern, qui rappresentato in UML:

state_pattern_uml

In pratica gli stati sono oggetti e la classe Context contiene il puntatore allo stato attuale.
Secondo il diagramma UML di cui sopra, la logica per calcolare le transizioni di stato si trova all’interno dei singoli stati (il metodo goNext di State1 richiama infatti il metodo setState di Context passandogli lo stato successivo State2).
Alternativamente si può mettere questa logica all’interno del contesto.
Il primo metodo è, secondo me, più leggibile, anche se ha lo svantaggio che gli stati si devono conoscere tra loro, in altre parole sono accoppiati.
Il secondo metodo non ha questo problema di accoppiamento, ma rende più difficile seguire il flusso logico della macchina a stati quando si legge il codice.
Inoltre esiste anche in questo caso un accoppiamento, questa volta tra il contesto e gli stati.

Nell’implementazione ho usato il primo metodo.
Mi è venuto comodo identificare il contesto con la macchina a stati: infatti è qualcosa che tiene traccia dello stato corrente e contiene i metodi con cui il client può comandare i passaggi di stato.

Questa è l’interfaccia con cui ho rappresentato il contesto:

public interface IStateMachine 
{ 
	void goNext(); 
	void setState(IState state); 
	IState getState(); 
	void Log(string message); 
} 

Oltre ai metodi goNext() e setState() richiesti dal pattern, ho inserito getState() per poter leggere dal client lo stato attuale, e Log() per ovvi motivi di logging.

Questa è l’implementazione dell’interfaccia:

public class StateMachineAssemblyCycle : IStateMachine
{
    private IState _currentState;
    public StateMachineAssemblyCycle(AssemblyLine assemblyLine, IState startState)
    {
        _assemblyLine = assemblyLine;
        _currentState = startState;
    }
    public void goNext()
    {
        _currentState.goNext(this);
    }
    public void setState(IState state)
    {
        ...
        _currentState = state;
    } 
    public IState getState() { return _currentState; }
    public void Log(string message)
    {
        ... 
    }
}

Lo stato è rappresentato attraverso l’interfaccia IState

public interface IState
{
    void goNext(IStateMachine stateMachine);
}

Questo è il codice relativo ad uno stato specifico, in cui si attende che il robot abbia terminato la missione che gli è stata assegnata.
Sono evidenziate le righe che riguardano i passaggi di stato.

public class StateWaitForRobotMissionConclusion : IState
{
    private static IState _instance; 
    public static IState GetInstance() 
    { 
        if (_instance == null) 
            _instance = new StateWaitForRobotMissionConclusion(); 
        return _instance; 
    } 
    private StateWaitForRobotMissionConclusion() { } 
    public override void goNext(IStateMachine stateMachine) 
    { 
        AssemblyLine assemblyLine = GetAssemblyLine(stateMachine); 
        Request request = assemblyLine.GetRobotRequest(); 
        if (request == null) { 
            stateMachine.Log("Non trovata nessuna richiesta a Robot pendente"); 
            stateMachine.setState(StateErrorRobotMission.GetInstance); 
        } 
        else 
            switch (request.GetRequestStatus()) { 
                case Request.State.Running: { 
                    stateMachine.Log("Richiesta in corso"); 
                    stateMachine.setState(this); break; 
                } 
                case Request.State.Ended: { 
                    stateMachine.Log("Arrivata cassetta " + request.BoxNumber); 
                    if (request.BoxNumberConsistentWithRobotMission()) 
                    { 
                        stateMachine.Log(string.Format("Cassetta {0} coerente con la missione", request.BoxNumber)); 
                        request.AdvanceRobotMissionState(); 
                        stateMachine.setState(StateWaitForBoxEnteringAssemblyLine.GetInstance); 
                    } 
                    else 
                    { 
                        string errorMessage = 
                            assemblyLine.BoxNumberNotRead ? 
                            "Cassetta non letta" : 
                            string.Format("Cassetta non coerente con la missione: attesa {0}, rilevata {1}", 
                                          request.Missione.BoxNumber, 
                                          request.BoxNumber); 
                        stateMachine.Log(errorMessage); 
                        stateMachine.setState(StateErrorBoxNonConsistentWithRobotMission.GetInstance); 
                    } 
                    break; 
                } 
            } 
    }
    ...
}

Ogni stato deve essere istanziato una sola volta: sarebbe inutile e dispendioso in termini di memoria creare più copie dello stesso stato, dal momento che lo stato contiene solo codice da eseguire e non dei dati.
Ho risolto questa problematica rendendo lo stato un singleton: il costruttore è privato e il metodo GetInstance() fornisce sempre lo stesso oggetto registrato in una variabile static della classe (si noti che questa implementazione del singleton non è thread-safe, quindi non è adatta nel multi-threading).

Questo, infine, è il codice del client (il controller della macchina assemblatrice):

class AssemblyLine
{
    private IStateMachine _smCycle;
    public AssemblyLine(...)
    {
        _smCycle = new StateMachineAssemblyCycle(this, StateWaitingForCycle.GetInstance());
        //[...]
    }
    public void DoCycle()
    {
        //[...]
        if (isOk)
            _smCycle.goNext();
    }
    //[...]
}

Su Wikipedia si possono trovare molte altre informazioni sullo state pattern.
Il testo di riferimento è Design Patterns della Gang of Four.

 

Annunci

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google photo

Stai commentando usando il tuo account Google. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...