Caffeine-Powered Life

Back to Basics - State Machines, Part 2

We already covered the basic of state machines in my last post. Now, we’re going to grow the concept to something a little bit bigger.

Polymorphic State Objects

As our domain grows, defining state can become more complicated, and more things need to happen when state changes. Suppose we’re not only setting a boolean flag or an enum state value. Maybe we’re also going to start sending out emails, generating reports, or other similar domain-related events.

To start, we need a base task state. This state is “always bad”. From the base state, everything should fail. As an abstract base class, nothing should work here. As we implement concrete classes, we will override these failure conditions with proper implementations.


public abstract class TaskState

{

  protected readonly Task task;

  

  protected TaskState(Task task)

  {

    this.task = task;

  }

  

  public abstract TaskStatus Status { get; }

  

  public virtual void CanOpen

  {

    get { return false; }

  }

  

  public virtual void CanStart

  {

    get { return false; }

  }

  

  public virtual void CanWaitForApproval

  {

    get { return false; }

  }

  

  public virtual void CanComplete

  {

    get { return false; }

  }

  

  public virtual void Open()

  {

    throw new InvalidOperationException("Unable to open task");

  }

  

  public virtual void Start()

  {

    throw new InvalidOperationException("Unable to start task");

  }

  

  public virtual void WaitForApproval()

  {

    throw new InvalidOperationException("Unable to wait for approval");

  }

  

  public virtual void Complete()

  {

    throw new InvalidOperationException("Unable to complete task");

  }

}

The first thing we need to do is get from an unknown state to an Open state. Let’s revise our TaskStatus enum. The only thing that we must do here is make sure that Default is always the first option.


public enum TaskStatus

{

  Default, Open, Started, WaitingForApproval, Completed

}

We’re going to need a builder to set the state. Add this method to your TaskState class.


public static TaskState Build(Task task)

{

  if (task == null)

    throw new ArgumentNullException("task");

  

  switch (task.Status)

  {

    case TaskStatus.Open:

      return new OpenTaskState(task);

    case TaskStatus.Started:

      return new StartedTaskState(task);

    case TaskStatus.WaitingForApproval:

      retun new WaitingForApprovalTaskState(task);

    case TaskStatus.Completed:

      return new CompletedTaskState(task);

    default:

      return new DefaultTaskState(task);

  }

}

From the Default task state, the only thing we’re allowed to do is open the task. The polymorphic state object should show this relationship.


public class DefaultTaskState : TaskState

{

  public DefaultTaskState(Task task)

    : base(task) { }

    

  public override bool CanOpen

  {

    get { return true; }

  }

  

  public override void Open()

  {

    this.task.Status = TaskStatus.Open;

  }

}

Next, let’s look at the CompletedTaskState. We’re looking at it first only because it’s the easiest. A completed task is done. We can’t do anything. All of our CanXyz accessors will still return false, and all of our methods should still throw exceptions.


public class CompletedTaskState : TaskState

{

  public CompletedTaskState(Task task)

    : base(task) { }

}

So What’s the Point?

We’re now adding complexity and abstraction for a bit of simplification. When we move our CanXyz properties and action methods to the new state class, we move the problem, but we don’t really eliminate it. We still had one big TaskState class that did everything. Now, we’ve changed the game. We have little classes that only have knowledge about themselves and what they can do. The OpenTaskState class can only get to Started and nowhere else. The StartedTaskState can only get to Waiting for Approval and nowhere else. You only have to worry about where you are and where you can go next. When your picture starts to get big, you don’t want to concern yourself with more than you need to.

And there you have it. You now have a state machine design pattern that uses object polymorphism to change states.

Comments