Pregunta

I've had a go at implementing the state pattern with a bit of help from the following StackOverflow post:

state pattern

Good so far. I can now move the object(the document, to the next state, that of ACKNOWLEDGED.)

 public override void MoveNext(Document currDoc, IProcessor currProcessor)
    {                               
        TransitionTo<ACKNOWLEDGED>(() => new ACKNOWLEDGED(_factory, _context));
        currProcessor.LogTheChange(currDoc);  
        currProcessor.DoSomethingElse(currDoc)
    }

Now, in the process of changing the state, I want to perform other actions in a different class. I doubt that it's a good idea to couple the state and IProcessor.

I believe that the state should only be concerned with changing between 2 discreet states (in this case UNACKNOWLEDGED and ACKNOWLLEDGED).

How do the actual updates to the underlying object happen?As far as I can see, I've passed the doc object to the moveNext() method, just so that it can then be passed elsewhere.If this is incorrect, then how should my state object communicate with IProcessor in a decoupled manner, should it raise an event that IProcessor will handle? Or, should I pass the interface as an argument in the MoveNext() method? I suspect not.

¿Fue útil?

Solución

Hmm. An interesting problem I would say this. If the state transitions only ever have one effect (that of making IProcessor do something or some number of things), then the approach is probably fine as it is.

Even if many things might happen as a result of the state change, but the MoveNext() function is the only way this change is instigated, then it probably wouldn't be horrible just to add more processors and actions to the method. You might eventually have to worry about what happens if one of your processors throws an exception.

However, if the state change can be kicked off from many places (many functions like MoveNext(), or if the state can change itself based on conditions) then you will want to have entities that watch state changes. You could use this using some sort of publish and subscribe mechanism, or simply by convention as certain objects watch as messages are emitted by your state entity.

If it were me, I would probably hook my state object up to something like the spring.net eventing system (http://springframework.net/doc-latest/reference/html/quickstarts.html). However, there are probably other ways to do this in .net that don't involve spring.

Otros consejos

I don't think the caller of a class that uses the State pattern internally should ever be aware that the state machine exists. Therefore, I'm not a fan of the client code calling Documemt.MoveNext. It exposes to much implementation detail.

Here is an alternate implementation that hides the state pattern within the Document class. note that I used private inner classes to completely hide the details of the state machine while providing full access of the Document members from withing each state subclass. I still keep those inner classes in their own code file though to avoid code clutter in the Document class file.

Document.cs

partial class Document
{

    public Document()
    {
        // default/starting state
        this.TransitionToState<EmptyState>();
    }

    // misc data for example
    public int? caseNumber { get; private set;}
    public DateTime? WhenSubmitted { get; private set; }
    public DateTime? WhenAcknowlegded { get; private set; }
    public int? CompletionStatus { get; private set; }

    // transitions:  EMPTY -> ASSIGNED -> UNACKNOWLEDGED -> ACKNOWLEDGED -> COMPLETED
    private DocumentState State { get; set; }


    // state-related methods are forwarded to the current DocumentState instance

    public void AssignCase(int caseNumber)
    {
        State.AssignCase(caseNumber);
    }

    public void SubmitTo(object clientInfo)
    {
        State.SubmitTo(clientInfo);
    }

    public void Acknowledged(object ackInfo)
    {
        State.Acknowledged(ackInfo);
    }

    public void Complete(int statusCode)        
    {
        State.Complete(statusCode);
    }

    // events could be used for this callback as well, but using private inner 
    //  classes calling a private member is probably the simplest.

    private void TransitionToState<T>() where T : DocumentState, new()
    {
        // save prior for a moment
        DocumentState priorState = State;

        // this can be lookup from map instead of new() if you need to keep them 
        //  alive for some reason.  I personally like flyweight states.
        DocumentState nextState = new T();

        // activate the new state.  it will get notified so it can do any one-
        //  time setup
        State = nextState;
        State.EnterState(this);

        // let the prior state know as well, so it can cleanup if needed
        if (priorState != null)
            priorState.ExitState();

    }        

}

DocumentState.cs

partial class Document
{

    abstract class DocumentState
    {

        //--------------------------------------------
        // state machine infrastructure 
        //--------------------------------------------

        public void EnterState(Document context)
        {
            this.Context = context;
            Console.WriteLine("Entering state: " + this.GetType().Name); // debug only
            OnEnterState();
        }

        public void ExitState()
        {
            this.Context = null;
            OnExitState();
            Console.WriteLine("State that was exited: " + this.GetType().Name); // debug only
        }

        protected Document Context { get; private set; }

        //--------------------------------------------
        // a mirror of the document-manipulation methods that concerns states
        //--------------------------------------------

        public void AssignCase(int caseNumber)
        {
            OnAssignCase(caseNumber);
        }

        public void SubmitTo(object clientInfo)
        {
            OnSubmitTo(clientInfo);
        }

        public void Acknowledged(object ackInfo)
        {
            OnAcknowledged(ackInfo);
        }

        public void Complete(int statusCode)
        {
            OnComplete(statusCode);
        }

        //--------------------------------------------
        // state subclasses override the methods they need.  Typically not 
        //  all are needed by all states.  Default implementation is to
        //  throw an exception if a state receives and "unexpected" invocation.
        //--------------------------------------------

        protected virtual void OnAssignCase(int caseNumber)
        {
            throw new InvalidOperationException();
        }

        protected virtual void OnSubmitTo(object clientInfo)
        {
            throw new InvalidOperationException();
        }

        protected virtual void OnAcknowledged(object ackInfo)
        {
            throw new InvalidOperationException();
        }

        protected virtual void OnComplete(int statusCode)
        {
            throw new InvalidOperationException();
        }

        //--------------------------------------------
        // additional hooks that can be override if needed that signal the
        //  enter and exit of the state.
        //--------------------------------------------

        protected virtual void OnEnterState()
        {
        }

        protected virtual void OnExitState()
        {
        }

    }

}

The state classes (I added additional ones for illustrative purposes):

partial class Document
{

    // Represents an empty document waiting to get assigned a case #.  Once 
    //  that is satisfied, it performs its logic and triggers a state 
    //  transition to the next state.
    class EmptyState : DocumentState
    {
        protected override void OnAssignCase(int caseNumber)
        {
            // business logic
            Context.caseNumber = caseNumber;
            // write to log
            // etc, etc

            // goto next state
            Context.TransitionToState<AssignedState>();
        }
    }

}

partial class Document
{

    // Represents an document assigned a ase number but not submitted to a
    //  client yet.  Once that happens it performs its logic and the triggers a state 
    //  transition.
    class AssignedState : DocumentState
    {
        protected override void OnSubmitTo(object clientInfo)
        {
            // business logic
            Context.WhenSubmitted = DateTime.Now;
            // etc
            // etc

            // goto next state
            Context.TransitionToState<UnacknowledgedState>();
        }
    }
}

partial class Document
{        
    // you get the idea by now...
    class UnacknowledgedState : DocumentState
    {
        protected override void OnAcknowledged(object ackInfo)
        {
            // business logic
            Context.WhenAcknowlegded = DateTime.Now;

            // goto next state
            Context.TransitionToState<AcknowledgedState>();

        }
    }
}

partial class Document
{
    class AcknowledgedState : DocumentState
    {
        protected override void OnComplete(int statusCode)
        {
            Context.CompletionStatus = statusCode;

            Context.TransitionToState<CompletedState>();
        }

    }
}

partial class Document
{
    class CompletedState : DocumentState
    {
        // note there are no methods overriden.  this is the last state.
    }
}

And finally, Program.cs:

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();

        Document doc = new Document();

        Console.ReadLine();

        doc.AssignCase(123456);

        Console.ReadLine();

        doc.SubmitTo("clientAddress");

        Console.ReadLine();

        doc.Acknowledged("responseFromClient");

        Console.ReadLine();

        const int TERMS_REJECTED = 123;
        doc.Complete(TERMS_REJECTED);

        Console.ReadLine();

    }
}

Let me know if you have any questions.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top