Refactoring is a process of changing software in a way that doesn’t alter the external behavior of code and yet improves its internal structure. Refactoring is just like performance optimization as both involve carrying out code manipulations that don’t change the overall functionality of the program. The difference is the purpose: Refactoring is always done to make the code easier to understand and cheaper to modify.

Refactoring Process

Refactoring is generally done when there is a tangible and observable indication that there is a deeper problem in the system, these are called code smells. There is a lot of code smell which includes Long Method, Large Class, which generally indicates a method or class is doing more than a single thing.

In this post, I am going to talk about one of the code smell Repeated Switches, and how it can be related to State design pattern. Repeat Switches occur where the same conditional switching logic (either in a switch/case statement or in a cascade of if/else statements) pops up in different places. The problem with such duplicate switches is that, whenever a clause is added or needs to be updated, one has to find all the switches and update them.

State Pattern

State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class. The state pattern is closely related to Finite state machines.

Finite State Machine

The main idea is that, at any given moment, there’s a finite number of states in which a program can be. Within any unique state, the program behaves differently, and the program can be switched from one state to another instantaneously. However, depending on the current state, the program may or may not switch to certain other states. These switching rules, called transitions, are also finite and predetermined.

Implementing State Pattern

Implementing a finite state machine involves a series of steps:

  • Gather up all the states
  • Define the states using constants or enums and create an instance to hold the current state.
  • Gather up all the action that can happen in a state machine
  • Create a class that acts as the state machine. For each action, we create a method that uses conditional statements to determine what behavior is appropriate in each state.

Example

Let’s check how these steps are followed in case of a simple gate in metro or subway

  • Gate will have three states: Open, Close, and Process Payment.
  • Defining the states using constants and a variable to hold the current state.
    private final int OPEN_STATE = 0;
    private final int CLOSE_STATE = 1;
    private final int PROCESSING_STATE = 2;
    private int CURRENT_STATE;
  • Checking up all actions that can happen on the gates, Enter, PayOK, PayFail, and PayInitiated.

States and Action

StatesEnterPayOKPayFailPayInitiated
Open StateCloseOpenOpenOpen
Close StateCloseCloseCloseProcessing
Processing StateProcessingOpenCloseProcessing

  • For each action, create a conditional to determine the next state of the gate.
    public void enter() {
        if (CURRENT_STATE == OPEN_STATE) {
            System.out.println("Individual has enter the gate, closing gate");
            CURRENT_STATE = CLOSE_STATE;
        } else if (CURRENT_STATE == CLOSE_STATE) {
            System.out.println("Please pay to open the gate");
        } else if (CURRENT_STATE == PROCESSING_STATE) {
            System.out.println("Payment is in process, please wait");
        }
    }

Let’s test all these behaviors out and be done with our development. Sadly we know that is not true ;). New requirements for the state might come in the future and we are checking for states at each action at multiple places, which can introduce buggy behavior in the code and this is a code smell known as Repeated Switches.

Refactoring to State pattern

Before refactoring, we should have a test suite ready which we can use to verify the behavior. We will make small changes on our way and run the suites to verify the existing behaviors.

Steps for refactoring

  • Step-1: Extract methods from the actions for a given state using Extract Methods.

    public void enter() {
        if (CURRENT_STATE == OPEN_STATE) {
            enterForOpenState();
        } else if (CURRENT_STATE == CLOSE_STATE) {
            System.out.println("Please pay to open the gate");
        } else if (CURRENT_STATE == PROCESSING_STATE) {
            System.out.println("Payment is in process, please wait");
        }
    }

    private void enterForOpenState() {
        System.out.println("Individual has enter the gate, closing gate");
        CURRENT_STATE = CLOSE_STATE;
    }
  • Step-2: Using Slide Functions aggregate all actions of a given state.

    private void enterForOpenState() {
        System.out.println("Individual has enter the gate, closing gate");
        CURRENT_STATE = CLOSE_STATE;
    }

    private void payOkForOpenState() {
        System.out.println(unreachableStep);
        throw new IllegalCallerException(unreachableStep);
    }
    
    private void payFailForOpenState() {
        System.out.println(unreachableStep);
        throw new IllegalCallerException(unreachableStep);
    }

    private void payInitiatedForOpenState() {
        System.out.println("Payment is already processed");
    }
  • Step-3: These extracted function becomes the base of the new class representing a state. We need to have a uniform interface to hold different methods to be executed by a given state containing all actions.
    public interface GateState {
        void enter();
        void payOK();
        void payFail();
        void payInitiated();
    }
  • Step-4: Create a class for OpenState implementing GateState and add all the functions to the that state having implementation extracted from the parent class. All our test cases will start to fail. We need a new suite of tests for verifying these changing behaviors.

  • Step-5: Repeat the same steps for the other GateState: CloseState and ProcessingState.

  • Step-6: Finally Change the Gate class to have the current state only, and the actions delegating their actions to the respective GateStates.


    public class Gate {
        GateState gateState;

        public void enter() {
            gateState.enter();
        }

        public void payOK() {
            gateState.payOK();
        }

        public void payFail() {
            gateState.payFail();
        }

        public void payInitiated() {
            gateState.payInitiated();
        }
    }

Advantage of Refactoring to State Pattern

  • Localised the behavior of a state into its own class.
  • Removed the code smell Repeated Switch using polymorphism.
  • Closed the state for modifications and let it remain for extensions, whenever a new requirement for the state arrives let’s say to add Retired State or Out of Order State, the code will be open for extension.
  • Code has become easier to understand and cheaper to maintain.

Source Code

Code is hosted at @github, please reach out for suggestions.

References