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.