Implementing document workflow with the state pattern

July 23 2011

Yesterday I blogged about managing basic document state/status  as it moves through the concept of a workflow.  In the first version of the workflow there were no workflow related classes.  The workflow alluded to be the existence of a Status property on the object and that status object determined the step of the workflow and the status of the document, or Contract as in the case I was looking at.

Here I’ve continued my exploration by implementing  a proper version of the State Pattern. My only deviation is that I’ve made the State internal to my document, rather than expose a public setter and getter.  I think the only way it should be possible to change the state of the document is by calling the appropriate method.

I’ve altered my test case internal slightly to deal with the new API.  Now, rather than have an objects State update automatically, the client calls a method to invoke the state change, once all properties have been set.  If the object can validly transition to the desired state, it does so.  If not, an exception is thrown.

using System;
using NUnit.Framework;

namespace WorkflowTest
{
    [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"};
            document.StatusOne();

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

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

            document.PropertyTwo = "Value";
            document.PropertyThree = "Value";
            document.StatusTwo();

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

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

            document.PropertyTwo = "Value";
            var ex = Assert.Throws<Exception>(() => document.StatusTwo());
            Assert.That(ex.Message, Is.EqualTo("Cannot change to Status Two."));
        }

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

            document.PropertyThree = "Value";
            var ex = Assert.Throws<Exception>(() => document.StatusTwo());
            Assert.That(ex.Message, Is.EqualTo("Cannot change to Status Two."));            
        }

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

            document.PropertyFour = "Value";
            Assert.AreEqual(2, document.Status);
            document.StatusThree();
            Assert.AreEqual(3, document.Status);
        }

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

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

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

            document.PropertyTwo = null;
            var ex = Assert.Throws<Exception>(() => document.StatusOne());
            Assert.That(ex.Message, Is.EqualTo("Cannot change to Status One.")); 
        }   

        private static WorkflowDocument GetStatusOneDocument()
        {
            return new StatusOneDocument();
        }

        private static WorkflowDocument GetStatusTwoDocument()
        {
            return new StatusTwoDocument();
        }

        private static WorkflowDocument GetStatusThreeDocument()
        {
            return new StatusThreeDocument();
        }

        private class StatusOneDocument : WorkflowDocument
        {
            public StatusOneDocument()
            {
                _state = new WorkflowDocumentStatusOne(this);
                _propertyOne = "Value";
            }
        }

        private class StatusTwoDocument : StatusOneDocument
        {
            public  StatusTwoDocument() 
            {
                _state = new WorkflowDocumentStatusTwo(this);
                _propertyTwo = "Value";
                _propertyThree = "Value";
            }
        }

        private class StatusThreeDocument : StatusTwoDocument
        {
            public StatusThreeDocument()
            {
                _state = new WorkflowDocumentStatusThree(this);
                _propertyFour = "Value";
            }
        }
    }
}

I have declared an interface IWorkFlowDocumentState which represents the state transitions.  In a real world example these would be verbs such as Activate, BeginProcessing, and so on, which when successful would place the object in a status/state of “Active” or “Pending”, using my example from the previous blog entry.

Similar to my last blog entry, where the class for each state implemented the abstract base class, each different state class implements this interface.  The methods representing the transition from that state to the state each method represents.  In some case the state transition is invalid and it is in these cases exceptions are raised when the method is called.

Below are my concrete state classes.  If you compare them to the yesterdays version, you’ll see they’re pretty similar.

using System;

namespace WorkflowTest
{
    // OK this one is abstract :)
    public abstract class WorkflowDocumentState : IWorkflowDocumentState
    {
        protected IWorkflowDocument _document;
        
        protected WorkflowDocumentState(IWorkflowDocument document)
        {
            _document = document;
        }

        public abstract IWorkflowDocumentState StatusOne();

        public abstract IWorkflowDocumentState StatusTwo();

        public abstract IWorkflowDocumentState StatusThree();               
    }

    public class WorkflowDocumentStatusZero : WorkflowDocumentState
    {
        public WorkflowDocumentStatusZero(IWorkflowDocument document) : base(document) { }

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

        public override IWorkflowDocumentState StatusTwo()
        {
            throw new Exception("Cannot change to Status Two");
        }

        public override IWorkflowDocumentState StatusThree()
        {
            throw new Exception("Cannot change to Status Three");
        }
    }

    public class WorkflowDocumentStatusOne : WorkflowDocumentState
    {
        public WorkflowDocumentStatusOne(IWorkflowDocument document) : base(document)  { }

        public override IWorkflowDocumentState StatusOne()
        {            
            return this;
        }

        public override IWorkflowDocumentState StatusTwo()
        {
            if (!string.IsNullOrEmpty(_document.PropertyTwo) && !string.IsNullOrEmpty(_document.PropertyThree)) {
                return new WorkflowDocumentStatusTwo(_document);
            }
            throw new Exception("Cannot change to Status Two.");
        }

        public override IWorkflowDocumentState StatusThree()
        {
            throw new Exception("Cannot change to Status Three.");
        }
    }

    public class WorkflowDocumentStatusTwo : WorkflowDocumentState
    {
        public WorkflowDocumentStatusTwo(IWorkflowDocument document) : base(document) { }

        public override IWorkflowDocumentState StatusOne()
        {
            throw new Exception("Cannot change to Status One.");
        }

        public override IWorkflowDocumentState StatusTwo()
        {            
            return this;
        }

        public override IWorkflowDocumentState StatusThree()
        {
            if (!string.IsNullOrEmpty(_document.PropertyFour))
                return new WorkflowDocumentStatusThree(_document);
            throw new Exception("Cannot change to Status Three.");
         }
    }

    public class WorkflowDocumentStatusThree : WorkflowDocumentState
    {
        public WorkflowDocumentStatusThree(IWorkflowDocument document) : base(document) { }

        public override IWorkflowDocumentState StatusOne()
        {
            throw new Exception("Cannot change to Status One.");
        }

        public override IWorkflowDocumentState StatusTwo()
        {
            if (string.IsNullOrEmpty(_document.PropertyFour)) {
                return new WorkflowDocumentStatusTwo(_document);
            }
            throw new Exception("Cannot change to Status Two.");
        }

        public override IWorkflowDocumentState StatusThree()
        {
            return this;
        }
    }
}

Now my revised Document class,
namespace WorkflowTest
{
    public interface IWorkflowDocument
    {
        int Status { get; }

        string PropertyOne { get; set; }
        string PropertyTwo { get; set; }
        string PropertyThree { get; set; }
        string PropertyFour { get; set; }

        void StatusOne();
        void StatusTwo();
        void StatusThree();
    }

    public class WorkflowDocument : IWorkflowDocument
    {
        protected IWorkflowDocumentState _state;

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

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

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

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

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

        public int Status
        {
            get 
            {
                if (_state is WorkflowDocumentStatusZero)
                    return 0;
                if (_state is WorkflowDocumentStatusOne)
                    return 1;
                if (_state is WorkflowDocumentStatusTwo)
                    return 2;                
                return 3;                
            }
        }

        public void StatusOne()
        {
            _state = _state.StatusOne();
        }

        public void StatusTwo()
        {
            _state = _state.StatusTwo();            
        }

        public void StatusThree()
        {
            _state = _state.StatusThree();
        }
    }
}

Post a comment

comments powered by Disqus