Modelling and programming entities that have state and participate in workflow

Workflow and State changes in C# business applications.
July 22 2011

It’s a common enough problem:  How do you programme your domain model to handle entities that participate in workflow and have a state (status).  I’ve been mentally debating this one for a little bit now.  My first thought was, “Easy, the State Pattern”.  I’ve not had to implement this pattern but I’ve read enough pattern books to know it exists and it sounded like a good fit.  The entity in this case is a Contract for signing up for mobile phone airtime.

In the business world the Contract goes through the following workflow:

  1. New: Data is being entered but nothing has been submitted or saved yet.  Essentially the operator has selected the “New Contract” button.
  2. Submitted: the Contract has been submitted for approval and activation but has not yet been looked at by an “approver”
  3. Pending: The Contract has been opened by an approver and they are currently working on approving the Contract and hooking up airtime.
  4. Active: The Contract has been approved and is connected to the network. A phone number is assigned and calls can be placed.
  5. Suspended: The phone has been suspended with the network and can not be used on the network.  Phone can revert to Active after suspension.
  6. Closed: The Contract has been completed or terminated and is no longer connected to the network. Final state.

A Contract usually transitions through the states from 1 to 6.  It can transition between 4 and 5 at any time and can transition to 6 from 2, 3, 4, or 5.

I was thinking of having methods for each status on the Contract, so I’d call Submit(), View(), Activate(), Suspend(), Close() to put the contract in various status.  Perhaps I’d need to supply the various methods with all the data necessary to make the valid state change.  E.G., the Activate method might take the phone number and other information that is mandatory for an active phone.  But then I need the ability to change the properties of the phone number once it’s active (data binding) , so didn’t want to do that.

An entity status is defined by the combined state of all it’s properties, so to me it made sense to some how have the status change whenever all the appropriate properties had the correct values.  This means checking the object status each time any property changes.

Now that I’ve mentioned Contracts, lets forget them, because they contain far too many properties and rules to easily (without confusing the reader) demonstrate my point.  Instead of gone with WorkflowDocument, which is a generic document, such as a Contract document, that is used in any workflow.  I’ve created a few basic properties, enough to illustrate the state change possibilities I’ve mentioned above, in my Contract case.

 

public class WorkflowDocument : IWorkflowDocument
{
    protected string _propertyOne;
    public string PropertyOne 
    {
        get { return _propertyOne;  }
        set 
        { 
            _propertyOne = value;
            CheckStatus();
        }
    }

    protected string _propertyTwo;
    public string PropertyTwo 
    {
        get { return _propertyTwo;  }
        set
        {
            _propertyTwo = value;
            CheckStatus();
        }
    }

    protected string _propertyThree;
    public string PropertyThree 
    {
        get { return _propertyThree;  }
        set
        {
            _propertyThree = value;
            CheckStatus();
        }
    }

    protected string _propertyFour; 
    public string PropertyFour 
    {
        get { return _propertyFour;  }
        set
        {
            _propertyFour = value;
            CheckStatus();                
        }
    }

    public int Status { get; protected set; }

    private void CheckStatus()
    {
        // Rules for each state change are contained here.
        // This is messy and should be refactored into Status objects.
        if (Status == 0 && !string.IsNullOrEmpty(PropertyOne)) {
            Status = 1;
        }
        
        if (Status == 1 && (!string.IsNullOrEmpty(PropertyTwo) && !string.IsNullOrEmpty(PropertyThree)))
        {
            Status = 2;
        }
        
        if (Status == 2 && (!string.IsNullOrEmpty(PropertyFour))) 
        {
            Status = 3;
        }

        if (Status == 3 && string.IsNullOrEmpty(PropertyFour)) {
            Status = 2;
        }
    }
}

 

In the above I’m calling CheckStatus whenever a value changes (I could be a little more intelligent and check the value has actually change not just been assigned).  That’s how I would have implemented the behaviour without using the state pattern.  in the above I wanted the Status to change as soon as the actual state of the object changed.

Here are the unit tests I’m using, to give an idea of the functionality and behaviour.

[TestFixture]
public class WorkflowDocumentFixture
{
    [Test]
    public void Initial_status_is_zero()
    {
        var document = new WorkflowDocument();

        Assert.AreEqual(0, document.Status);
    }
    
    [Test]
    public void Can_change_to_status_one()
    {
        var document = new WorkflowDocument() {PropertyOne = "Value"};

        Assert.AreEqual(1, document.Status);            
    }

    [Test]
    public void Can_change_to_status_two()
    {
        var statusOneDocument = GetStatusOneDocument();

        statusOneDocument.PropertyTwo = "Value";
        statusOneDocument.PropertyThree = "Value";

        Assert.AreEqual(2, statusOneDocument.Status);
    }

    [Test]
    public void Does_not_change_to_status_two_if_only_property_two_set()
    {
        var statusOneDocument = GetStatusOneDocument();

        statusOneDocument.PropertyTwo = "Value";
        Assert.AreEqual(1, statusOneDocument.Status);
    }

    [Test]
    public void Does_not_change_to_status_two_if_only_property_three_set()
    {
        var statusOneDocument = GetStatusOneDocument();

        statusOneDocument.PropertyThree = "Value";
        Assert.AreEqual(1, statusOneDocument.Status);
    }

    [Test]
    public void Can_change_to_status_three()
    {
        var statusTwoDocument = GetStatusTwoDocument();

        statusTwoDocument.PropertyFour = "Value";
        Assert.AreEqual(3, statusTwoDocument.Status);                       
    }

    [Test]
    public void Can_change_back_to_status_two_from_status_three()
    {
        var document = GetStatusThreeDocument();
        
        Assert.AreEqual(3, document.Status);
        document.PropertyFour = null;
        Assert.AreEqual(2, document.Status);
    }

    [Test]
    public void Cannot_change_back_to_status_one_from_status_two()
    {
        var document = GetStatusTwoDocument();

        document.PropertyTwo = null;
        Assert.AreEqual(2, document.Status);
    }
}

 

The state pattern goes a little further and isolates the state change logic into separate classes.  In the next snippet I haven’t gone to the extreme of the state pattern proper, as I didn’t want to call entity.Activate() just yet. I wanted to have the state change when the property changed. That meant a few modifications to my WorkflowDocument class above.

public class WorkflowDocumentWithState : IWorkflowDocument
{
    protected WorkflowDocumentState _state;

    public WorkflowDocumentWithState()
    {
        _state = new WorkflowDocumentStatusZero(this);
    }

    protected string _propertyOne;
    public string PropertyOne
    {
        get { return _propertyOne; }
        set
        {
            _propertyOne = value;
            CheckStatus();
        }
    }

    protected string _propertyTwo;
    public string PropertyTwo
    {
        get { return _propertyTwo; }
        set
        {
            _propertyTwo = value;
            CheckStatus();
        }
    }

    protected string _propertyThree;
    public string PropertyThree
    {
        get { return _propertyThree; }
        set
        {
            _propertyThree = value;
            CheckStatus();
        }
    }

    protected string _propertyFour;
    public string PropertyFour
    {
        get { return _propertyFour; }
        set
        {
            _propertyFour = value;
            CheckStatus();
        }
    }

    public int Status { get { return _state.Value; }
    }

    private void CheckStatus()
    {
        _state = _state.Validate();            
    }
}

The two main changes are the addition and use of the WorkflowDocumentState class, which is an abstract based class for the various states the WorkflowDocument can be in.

public abstract class WorkflowDocumentState
{
    protected IWorkflowDocument _document;
    protected int _value; 

    public int Value { get { return _value; } }
    
    protected WorkflowDocumentState(IWorkflowDocument document)
    {
        _document = document;
    }

    public abstract WorkflowDocumentState Push();
}

 

The Push method will be called to push the document through the workflow to the next the state.  I’ve implemented concrete WorkflowDocumentState classes that contain the logic that was previous in my original CheckStatus method in WorkflowDocument.

public class WorkflowDocumentStatusZero : WorkflowDocumentState
{
    public WorkflowDocumentStatusZero(IWorkflowDocument document) : base(document)
    {
        _value = 0;
    }

    public override WorkflowDocumentState Push()
    {        
        if (!string.IsNullOrEmpty(_document.PropertyOne)) {
            return new WorkflowDocumentStatusOne(_document).Push();
        }
        return this;
    }
}

public class WorkflowDocumentStatusOne : WorkflowDocumentState
{
    public WorkflowDocumentStatusOne(IWorkflowDocument document) : base(document) 
    {
        _value = 1;
    }

    public override WorkflowDocumentState Push()
    {
        if (!string.IsNullOrEmpty(_document.PropertyTwo) && !string.IsNullOrEmpty(_document.PropertyThree)) {
            return new WorkflowDocumentStatusTwo(_document).Push();
        }
        return this;
    }
}

public class WorkflowDocumentStatusTwo : WorkflowDocumentState
{
    public WorkflowDocumentStatusTwo(IWorkflowDocument document) : base(document)
    {
        _value = 2;
    }

    public override WorkflowDocumentState Push()
    {
        if (!string.IsNullOrEmpty(_document.PropertyFour)) {
            return new WorkflowDocumentStatusThree(_document).Push();
        }
        return this;
    }
}

public class WorkflowDocumentStatusThree : WorkflowDocumentState
{
    public WorkflowDocumentStatusThree(IWorkflowDocument document) : base(document)
    {
        _value = 3;
    }

    public override WorkflowDocumentState Push()
    {
        if (string.IsNullOrEmpty(_document.PropertyFour)) {
            return new WorkflowDocumentStatusTwo(_document).Push();
        }
        return this;
    }
}

The pattern I’ve followed in each of my concrete Push methods is to check the properties of the entity to see it meets the conditions of a state change.  Generally calling Push would check if the entity is ready to move along the workflow.  If an entity meets the conditions of a state change, the new state is created and Push is called on it.  this makes it possible to create a New object and fill in enough properties to have it seemlessly progress through to active, as would happen when an Activator in my Contract scenario is the person signing up new contracts.

One thing I should have included was the ability for a WorkflowDocumentState to store the state of the document when it first transitioned into that state. None of my basic tests required it though, so I did not.

I’m now going to investigate using a fully fledged state pattern, where I call entity.Activate() when I’m ready to active a contract.  But for now, this is a step in the right direction.

Post a comment

comments powered by Disqus