Scaling State Altering Conditional Logic With State Pattern

Scaling state altering conditional logic with State Pattern

In this post I am going to talk about State pattern and its application. But before we start talking about the mechanics I would like to share a few thoughts on when and why we should introduce a certain design pattern in the code.

This is the seventh post of our blogger month at Xebia. Every day one of the Xebian post a new blog. You can follow the full series at http://blog.xebia.in/pages/blogger-month-2015/.

Why do we need design patterns?

Design patterns address problems in the code that make the code harder to extend, test and understand (especially for others). These problems may exist in a piece of code that works perfectly fine as far as the correctness is concerned and are fixed via refactoring the code harnessed with a set of test cases. Refactoring of code is always the next step once we have the code working correctly, there is no point in having some code that has no problems architecturally but doesn't work as per the expectations.

Design patterns are a set of design techniques that define a way to make your code extensible, readable and sometimes more testable. However, the application of a certain design pattern should not be driven by the fact that the developer knows that pattern, it should always be the outcome of an opportunity presented by a working piece of code to the developer. When we say working piece of code, it means the code is well covered by unit test cases with the right set of assertions. Thinking of a business problem with the perspective of a design pattern is not the right thing to do, because a pattern would take care of design flaws and not the functional shortcomings in the code. Therefore the business problem should be solved first for correctness and then the code solving that problem should be made beautiful.

With whatever has been said above I want to make it clear that refactoring of code to make it adhere to the design principles is equally important. It has no different precedence whatsoever , its just the order in which these things should be done, i.e
– correctness
– refactoring

In the section that follows next we will look at one such scenario where State Pattern can be applied. Also we'll see what makes a certain implementation a right candidate for such a refactoring. Let's talk about what State pattern is and what are the design problems that it addresses.

State Pattern helps us achieve following objectives :

  • Allows an object to alter its behavior when its internal state changes
  • Facilitates seamless transition of the Context from one state to other
  • Simplifies the state altering conditional logic
  • Makes code scale to handle more state transitions without convoluting the logic
  • Adheres to the Open Close principle

We will list a few symptoms that are present in the code that would make the code a candidate for refactoring to State Pattern

  1. Too many conditions in the logic that facilitates state change of the context
  2. Addition of more state transitions that may complicate the conditional logic further
  3. State altering logic is not readable
  4. General code smells w.r.t Duplication, Communication and Simplicity

Now we will pick a simple problem that would give us an opportunity to refactor our code to get to State pattern. The problem is defined as follows

We have an Agile board where multiple tasks can be part of a Sprint. Each Task in its lifecycle undergoes various transitions. Once a Task is created and added to a sprint its status is set to TO-DO, it then makes transitions to states like in-progress, testing etc before it finally gets done.

There are a few constraints with respect to each transition e.g a task in state To-Do won't be able to go to testing without entering an in-progress state.
Lets see the first working implementation of class AgileTask

public class AgileTask {

    private static final String TO_DO = "TODO";

    private static final String IN_PROGRESS = "IN_PROGRESS";

    private static final String IN_TEST = "IN_TEST";

    private static final String DONE = "DONE";

    private String state;

    public AgileTask() {
        this.state = TO_DO;
    }

    public void assigned() {
        if (state.equals(TO_DO) || state.equals(IN_PROGRESS)) {
            this.state = IN_PROGRESS;
            System.out.println("Task moved to "+IN_PROGRESS);
        }else{
            error();
        }

    }

    public void moveToTest(){
        if(state.equals(IN_PROGRESS)){
            state=IN_TEST;
            System.out.println("Task moved to "+IN_TEST);
        }else{
            error();
        }
    }

    public void reOpen(){
        if(state.equals(DONE)){
            state=TO_DO;
            System.out.println("Task has been reopened");
        }else{
            error();
        }
    }

    public void done(){
        if(state.equals(IN_PROGRESS)){
            state=DONE;
            System.out.println("Task has been reopened");
        }else{
            error();
        }
    }

    private void error() {
        throw new IllegalStateException("Cannot make the transition from current state");
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state=state;
    }
}

Lets see if above listed symptoms are present in the AgileTask class

  • Symptom 1 : Yes. there are too many if else conditions present in the logic. As we define more possible transitions from one state to others , the if-else mesh would grow further in each method.

  • Symptom 2: Yes, Addition of a new state to existing set would not just lead us to write its possible transitions in an if-else construct, it would also increase the number of possible combinations and would force us to make changes to pre-existing code. This increases complexity of already complicated code and also violates Open Close principle grossly.

  • Symptom 3: Yes. Readability already has taken a hit with the current implementation and is going to deteriorate further as we add more conditions and state transitions.

  • Symptom 4: Indeed. Simplicity is masked with if else constructs, communication is marred by too many things happening at one place. Duplicate code is all over the place.

All 4 symptoms are indicating that scaling the above implemented logic to more state transitions is not going to happen without making the code an absolute mess or spaghetti, if you like. So it seems we have reasons enough to refactor this code to State Pattern, Lets see what are the mechanics we may want to follow in order to achieve it.

Before we start to refactor the code we should have the AgileTask covered with a test which should be green at the start and the end of the refactoring we are about to perform.

public class AgileTaskTest {

    @Rule
    public ExpectedException expectedException=ExpectedException.none();

    @Test
    public void shouldMoveTaskToInProgressFromToDo() throws Exception {
        AgileTask agileTask = new AgileTask();
        agileTask.assigned();
        assertEquals(agileTask.getState(),"IN_PROGRESS");
    }

    @Test
        public void shouldThrowExceptionIfTriestoAssignADoneTask() throws Exception {
            AgileTask agileTask = new AgileTask();
            agileTask.setState("DONE");
            expectedException.expect(IllegalStateException.class);
            expectedException.expectMessage("Cannot make the transition from current state");
            agileTask.assigned();
        }

        @Test
        public void shouldMoveTaskToTestFromInProgress() throws Exception {
            AgileTask agileTask = new AgileTask();
            agileTask.setState("IN_PROGRESS");
            agileTask.moveToTest();
            assertEquals(agileTask.getState(),"IN_TEST");
        }
      ......
}

Mechanics

Step 1

Identify the class which holds a State at any given moment as Context. That would be the AgileTask object that holds the state at any given moment.

Step 2

Create an abstract State class and name it in accordance with the purpose of the state field. Let's name that class TaskState for this example.

Step 3

Declare subclasses to the abstract TaskState class defining each state that the Context object may enter e.g ToDo is a state that task may enter thus

   public class ToDo extends TaskState{...}

similarly

   public class InProgress extends TaskState{...}

Step 4

Create a field of type abstract state in the Context class and introduce a mechanism to set/inject it

public abstract class TaskState {
    private final String name;

    public TaskState(String stateName) {
        this.name=stateName;
    }
}

Step 5

Identify methods in the context class that alter state of the context and move it to the abstract state type

public abstract class TaskState {
    private final String name;

    public TaskState(String stateName) {
        this.name=stateName;
    }
    public void assigned(AgileTask task) {
      error();
    }

    public void moveToTest(AgileTask task) {
        error();
    }

    public void reOpen(AgileTask task) {
        error();
    }

    public void done(AgileTask task) {
        error();
    }

    private void error() {
        throw new IllegalStateException("Cannot make the transition from current state");
    }
}

The reason every method throws an IllegalStateException is that TaskState is an abstract type and no valid transition can take place from here.

Step 6

Implement all relevant methods for the defined subclass of the abstract TaskState class. Relevant methods would be the transitions that may take place from a certain state, for instance for state ToDo only relevant transition is Assigned, thus it only overrides the assigned method from TaskState class. Any transition that a class does not define are not valid transitions at the end of step 8 we would notice that all irrelevant state transitions are sent to the TaskState class which suggests the transition as an invalid one by throwing an IllegalStateException. To avoid creating state objects every time we would add different states in the TaskState class as constants as follows.

public abstract class TaskState {

  protected static final TaskState TO_DO = new ToDo("TODO");

  protected static final TaskState IN_PROGRESS = new InProgress("IN_PROGRESS");

  protected static final TaskState IN_TEST = new InTest("IN_TEST");

  protected static final TaskState DONE = new Done("DONE");
  .....
}
public class ToDo extends TaskState {

  public ToDo(String todo) {
      super(todo);
  }
  @Override
  public void assigned(AgileTask task) {
      task.setState(TaskState.IN_PROGRESS);
  }
}

Step 7

Repeat the above step(numbered 6) for each of the state altering method that is moved out to abstract state class e.g class InProgress looks like below.

public class InProgress extends TaskState {
    public InProgress(String in_progress) {
        super(in_progress);
    }
    @Override
    public void assigned(AgileTask task) {
        task.setState(TaskState.IN_PROGRESS);
    }
    @Override
    public void moveToTest(AgileTask task) {
        task.setState(TaskState.IN_TEST);
    }
}

repeat this for states InTest and Done.

Step 8

Remove the body of all irrelevant methods for a concrete state class and delegate call to the default implementation. Now the AgileTask class looks like below

public class AgileTask {
    private TaskState state;
    public AgileTask() {
        this.state = TaskState.TO_DO;
    }
    public void assigned() {
        state.assigned(this);
    }
    public void moveToTest() {
        state.moveToTest(this);
    }
    public void reOpen(){
        state.reOpen(this);
    }
    public void done(){
       state.done(this);
    }
    public TaskState getState() {
        return state;
    }
    public void setState(TaskState state) {
        this.state = state;
    }
}

Step 9

Compile and Test.

If at the end of step 9 all tests are green then we are done with the refactoring and the code can now be checked in to the VCS.

Lets look back and see if this refactoring has gotten rid of the four symptoms that we noted earlier.

  • Symptom 1 : No longer as the conditional logic has been replaced with sub class based polymorphism, transition methods are simply making switches from one state to the other without checking current state explicitly.

  • Symptom 2 : Gone. Scaling code to handle more transitions would not involve changing existing code, add a new state class(TaskState extension) ,
    implement relevant transitions and that is it.

  • Symptom 3 : Much Improved. Reading a State class gives a concise information about what all are valid transitions for this particular state.

  • Symptom 4 : This symptom is left for the reader to analyze.

The code for the post can be found here

Leave a Reply

Your email address will not be published. Required fields are marked *