Design Patterns: Refactoring an If/Then/Else State Handler using the State Pattern

Posted on August 1, 2011

0


Lets consider you wrote this code to save files either locally or over a network connection (the values in capitals are Constants used in your code instead of hard coded values):

class MyFile()
{
 // Set our starting position
   var dataState=DATA_STATE_UNCHANGED;
   var fileSaveState=FILE_SAVESTATE_UNSAVED;
 // Store our data
   binaryData:BinaryData=new BinaryData(); 
   public method saveFile()
   {
      if(dataState==DATA_STATE_UNCHANGED)
      {
        MessageCentral.showMessage("File has not changed. No need to save it");
        return;
      }
      if(dataState==DATA_STATE_CHANGED)
      {
        if(fileSaveState==FILE_SAVESTATE_SAVING)
        {
 // We decided to show a message and exit
          MessageCentral.showMessage("Already saving the file now, please try again later");
          return;
        }
        if(locationType==LOCATION_TYPE_REMOTE)
        {
 // Create object and open connection to save it remotely
      fileHander=new FileHandlerRemote();
 // Attempt to save file
          remoteSaveAttemptSuccesful=fileHandler.saveFile(binaryData,location); 
 // Attempt to save
 // Give feedback if save failed
 // Set flag to indicate if we succeeded in our attempt
          saveAttemptSuccesful=remoteSaveAttemptSuccesful;
       }
       if(locationType==LOCATION_TYPE_LOCAL)
       {
 // Open connection to save it locally
         fileHandler=new FileHandlerLocal();
 // Attempt to save
         localSaveAttemptSuccesful=fileHandler.saveFile(binaryData,location);
 // Give feedback if save failed
 // Set flag to indicate if we succeeded in our attempt
         saveAttemptSuccesful=localSaveAttemptSuccesful
      }
 // Done. Now change the state if we failed
      if(saveAttemptSuccesful==false)
      {
 // Make sure we set the right state
        fileSaveState=FILE_SAVESTATE_UNSAVED;
 // Exit
        return;
      }
   }
// We need an event handler when Saving (attempt) is done
   private method onSaveFinished(evt:Event)
   {
   // Here we get feedback from our process  
   // As it is from an asynchronous process, we only know now if it failed or not
      if(evt.success==true)
      {
         fileSaveState=FILE_SAVESTATE_SAVED;
         MessageCentral.showMessage("Your file has been saved");
      }
      if(evt.success==false)
      {
         fileSaveState=FILE_SAVESTATE_UNSAVED;
         MessageCentral.showMessage("Oops! Something went wrong. Your file has not been saved");

      }
 // Done
   }
}

If this was a real world example, we would have handlers for IO errors as well, and be more specific on why stuff went wrong, so the user can correct if possible.

Issues with the If/Then/Else approach

What is “wrong” in this approach is the following:

  1. Too much responsibility/things happening in one single place – Many decisions are made and managed in one block of code: saveFile
  2. Allows for clutter to happen – The more exceptions you get (captured with If/Then/Elses or Switch vases) the more mess your code will contain.
  3. Too rigid/resilient against change – If your pattern changes or new states are added, you will have to add (and change) If/Then/Else statements. This can be a very tough process and can introduce (new) bugs when done improperly or by someone else.

Looking at the parts we have

To continue our review of the code above: we can split the if/then/else in “saveFile” into two main decision making:

  1. The save- and data-state of the file – Data is changed/unchanged and the file is saved/unsaved.
  2. The way we store the data – Remote or locally. As there is no business logic present for this, for now, we can use a “Simple Factory” to select a handler. There are two advantages to that:
    1. Extendability – We can extend the number of possible handlers to any possible number
    2. Clean code – We move the (potential) clutter to another class and pass only the known stuff to have it resolved.

The File Handler

The way we store data, can be outsourced to an external class, called – for instance – “FileHandler”.

We will skip this class and assume it is there in the next parts, to keep this example simple.

Using State Pattern – The MyFile State Handlers

Before we start: As we are going to implement multiple Handlers, we need a generic interface. There are several ways to do this, including inheritance of a Interface, extending an Abstract class or extending a Template class.

We use an Interface: As we assume that we do not share any generic code, we use an Interface to allow the application to use all different SavedState Classes as if they are the same thing. An Interface is basically a class with just the definition of the Methods but no code.

Two State Types interacting: We also have two state types interacting with each other and influencing the possible next steps of each other:

  1. Changed state – The file  is changed/unchanged/being saved
  2. Saved state – The file is saved/unsaved/in the process of being saved

One of the solutions might be to create a state for each combination as we have defined it now. Currently leading to 3 x 3 = 9 different possible states.

State Manager: Instead we do something else: we use a State Manager – introduced later in this example. I explain later the reasoning behind this.

State Handler for unsaved files:

class FileState_UnsavedHandler implements IFileStateHandler
{
    public method saveFile(fileData, location, method, 
                           callBackSuccessful,callBackFailed)
    {
        // The file is unsaved

        // Create a File Handler         
 fileHandler:FileHandler= new FileHandler()
        // Attempt save with passed parameters
        fileHandler.saveFile(binaryData,location, method,
                             callBackSuccessful,callBackFailed)
        // Notify the user
        MessageCentral.showMessage("Attempting to save the file");
        // Return the new state
        return FILE_SAVESTATE_SAVING        // Done
   }
}

State Handler for saved files:

class FileState_SavedHandler implements IFileStateHandler
{
    public method saveFile(fileData, location, method,
                           callBackSuccessful,callBackFailed)
    {
        // Notify the user
        MessageCentral.showMessage("File is already saved, no action taken");
 // Return the state
        return FILE_SAVESTATE_SAVED; 
   }
}

State Handler for files which are in the process of saving:

class FileState_SavingHandler implements IFileStateHandler
{
    public method saveFile(fileData, location, method,
                           callBackSuccessful,callBackFailed)
    {
 // The fileData is already being saved
 // Notify the user
        MessageCentral.showMessage("File is already being saved,   no action taken");
 // Return state
        return FILE_SAVESTATE_SAVING; 
   }
}

What we have so far: We have three basic State Handlers for all three currently defined Saved states. Two are basically doing nothing more than giving feedback to the user. The question that might have arisen in your mind at this point is: what is the use of this?

What we did: We encapsulated different possible scenarios for when the user wants to save stuff into separate classes, which are interchangeable as they all use the same interface.

What this delivers: Clean code and a clear purpose. Later, when your application starts growing and decisions are becoming increasingly complex, encapsulation and separation of State specific actions will help you keep control over the processes that you manage in these classes.

What we do not have yet: We still need to deal with the Changed State.

What we avoided: In many textbook examples you will find:

  1. References to the class we handle the State Specific processes for. In our case case that would be the Class: “MyFile”.
  2. Direct manipulation of variables on “MyFile” by the State Handlers
  3. Interference with other processes. For instance: we could also set the File Changed State to “unchanged” when a file is saved.

This leads to tight coupling and rigid code.

What can go wrong: Each State returns or sets the next state to be executed (including itself, if nothing changed). If we make a mistake in this (for instance stating the new state is “Saved” while we are actually still saving the file), the process will “get lost” or “break” and the application might end up in places we did not intend it to go.

Why you might want to consider the Mediator Pattern: This dependency of “internal decision making per State Handler” and the fragility of the chain is one of the reasons you might want to consider using the Mediator Pattern.

Using a State Manager: Another alternative is using/adding a State Manager to your State Pattern. The State Manager adds the benefits of centralized decision making and centralized sanity checks to the clarity that encapsulation of State Specific responses delivers.

Tight Coupling?: Means that both your: “MyFile” Class and your State Handlers have awareness of each others existence. This might seem OK at the beginning, but can lead to a lot of trouble in later stadia of your code. Your code and your solutions to a problem should be disposable. I will explain in the next blocks why.

Rigid code: Meaning that one specific type of solution is hardcoded into many places, leading to a lot of trouble if we want to discard of it.

Avoiding three issues: Adding The State Manager to the equasion

Purpose of the State Manager: As our decision making process covers two State Types (Changed and Saved) we might want a Manager to deal with the potential complexity that arises from managing the different states in different combinations.

What it solves: We have three issues when dealing with the State Pattern:

  1. Are we still doing the right thing? – The State Manager can execute sanity checks on States which are centralized. These sanity checks can help you avoid entering the wrong state when specific Exceptions occur.
  2. How do we deal with dependencies between mixed State Types? – Like the possibility of having any combination of: “Saved State” and: “Changed State” in our example, already leading to 9 different possible combined States. State Handlers are relatively single minded. And the purpose of a State Handler is to replace If/Then/Else constructions by encapsulating specific responses to specific states in specific classes.
  3. How do we decouple our State Handlers from the “MyFile” Class? – Tight Coupling of a solution to the Class that requires that solution generates a lot of problems when change is required. We rather decouple the State Handlers from the “MyFile” Class, to become more Agile in our solution.
In contrary to each State Handler, our State Manager is able to oversee the total result of any State Change and take action when one State Change affects the State of another State Type. For instance: when a file is saved, the state for Saved should be changed to: “file is saved” and (only) when the File Data was not changed in the mean time (having the status “being saved”) we can then reset the File Data state to “data is not changed”.

Disposable code: When you start building code with refactoring in mind, your code and your solution should be disposable. Meaning that if something better comes along, you can drop the old one and inject the new.

Plug and Play: What we create via our State Manager is a “plug and play” variant of a State Pattern, achieving that exact goal.

Using a State Manager, we decouple the State Handlers from the Class we manage the state of. This leads to several useful results:

  1. The managed object can remain simple – As it is only required to do what it is intended to do
  2. We become more agile in our possible solutions – As we do not force a specific solution onto the managed object, bet let something else deal with that, we can remove one approach plug any other into it instead

Here we go:

class FileStateManager
{
   // Define the variables to keep our state
   var savedState:String=FILE_SAVESTATE_SAVED;
   var changedState:String=DATA_STATE_UNCHANGED;
 // Mappings
   fileStateHandlers:Dictionary<String,IFileStateHandler>=
            new Dictionary<String,IFileStateHandler>()
   changedStateHandlers:Dictionary<String,IChangedStateHandler>=
            new Dictionary<String,IDataStateHandler>()
   public method init()
   {
 // Did we already do this?
      if(fileStateHandlers.length>0)
      { 
 // Already done
        return;
      }
 // Map it!
      fileStateHandlers[FILE_SAVESTATE_UNSAVED]=new FileState_UnsavedHandler();
      fileStateHandlers[FILE_SAVESTATE_SAVED]=new FileState_SavedHandler();
      fileStateHandlers[FILE_SAVESTATE_SAVING]=new FileState_SavingHandler();
      changedStateHandlers[DATA_STATE_CHANGED]=new DataState_ChangedHandler();
      changedStateHandlers[DATA_STATE_SAVEATTEMPT]=new DataState_SaveAttempt();
      changedStateHandlers[DATA_STATE_UNCHANGED]=new DataState_UnchangedHandler();

   }
// Handle edit event from Class "MyFile"
   public method changeEventHandler(evt:Event)
   {
 // Get the handler
  var handler:IChangedStateHandler=changedStateHandlers[this.changedState];
 // Perform the actions required and return our new state
  this.changedState=handler.handleEdit(evt.data);
 // Handle busines logic when file is changed and was saved
 if(this.savedState==FILE_SAVEDSTATE_SAVED && this.changedState==DATA_STATE_CHANGED)
     {
 // Data State is changed and no longer "saved"
        this.savedState=FILE_SAVEDSTATE_UNSAVED;
      }
   } 
// Handle call to save file
   public method saveFile(fileData, method,
                          callBackSuccessful,callBackFailed)
   {
  // Get the handler
      var handler:IFileStateHandler=fileStateHandlers[this.savedState];
      // Perform the action
      this.savedState=handler.saveFile(fileData, method, this.changedState
                       saveSuccesfulCallback, saveFailedCallback) 
      // We deal with the changed state when we get the call back
 // Make sure we reflect the save state for data too
      if(this.savedState==FILE_SAVEDSTATE_SAVING)
      {
         this.changedState=DATA_STATE_SAVEATTEMPT;
      }
   }
   private method saveSuccessfulCallback(evt:Event)
   {
      // Save is succesful, we can set our state to saved
      this.savedState=FILE_SAVEDSTATE_SAVED;

 // If the data has not changed in the meantime we can set it to unchanged
      if(this.changedState==DATA_STATE_SAVEATTEMPT)
      {
         this.changedState=DATA_STATE_UNCHANGED; 
      } 
 // In all other cases, the data either is unchanged
 // or new changes were made after our save-attempt
   }
}

What we have: We now have a simple State Manager to manage the dependencies of our two different state types.

All our decision making is externalized in interchangeable classes which we can expand as much as we want, without making our central State Management.

If we want to implement a different process or different rules to manage the responses of our application to the state of a file, we can simply replace this manager for another that implements that process or those (new) rules.

Did we really win something? We started with a simple If/Then/Else solution where we solved this in maybe 20 lines of code and ended up with more than six classes and a hell of a lot more than those six lines of code.

What we won with the State Manager: Three things:

  1. Centralized decision making on mixed states – We decoupled all decision making on mixed states from the State Handlers, so that they can focus on their main task: handling one specific state
  2. One single point of entry – for our “MyFile” Class to have its States handled and managed.
  3. Disposable solution – Which is easy to replace by something completely new and improved.

Revisiting the “MyFile” Class

class MyFile()
{
 // Get a File State Manager for this file
   var stateManager:FileStateManager=new FileStateManager();

 // File data
   var binaryData:BinaryArray=new BinaryArray(); 
   // Constructor
   public MyFile()
   {
 // Add an eventlistener and bind it to the stateManager   // (to keep the example simple)
     binaryData.onChange=stateManager.changeEventHandler;
   }
   public method saveFile(method)
   {
 // We now have a manager to save our file:
 // dealing with any possible reaction to any
 // of our possible strategies and States
      stateManager.saveFile(this.binaryData, method)

 // "Method" is either stating to save it Remote or Local
   }
}

What we have: We now have a simplified Class: “MyFile”. Its purpose has become more clear as  all our decision making and action code has moved elsewhere.

What we won: Our code in the “MyFile” class is incredibly clean.

We can now easily switch from one State Manager to another, allowing us to completely revise our strategy in solving things and saving files, for instance when the requirements change from:

  1. Blocking state – Files can not be edited while being saved.
To
  1. Non blocking state – Files can be edited while being saved to a remote location
  2. Ignore “save” when saving still in progress – When a file has been edited but the previous version is still being saved, we ignore the press on the “Save button”
To
  1. Que “save” when saving is till in progress and execute last “save” instruction after “save” is done – Where we assure that when the user presses: “save”, this instruction is executed.

Closing thoughts on this refactoring

  1. There are many ways to implement the State Pattern – I choose the way closest to my own likings. This includes the use of Mapping of my State Objects and the use of String variables to define the current State (instead of injecting the State Object into a variable in the Class I “manage” with the State Pattern)
  2. The basic State Pattern is assumed to be Self Managing – You will find most textbook examples (including those in “Design Patterns” and “Design Patterns for Dummies”) to be self managing Single State Type solutions. You will not find a State Manager.
  3. The text book State Pattern fails on combinations of State Types – When your application has multiple State Types (like “Changed State” and “Saved State”), your number of potential combination of states grows. If you do not add a middle-layer (like the State Manager) to your solution, your State Pattern is going to be a very complicated mess.
  4. Not every If/Then/Else construction is a State Pattern – What makes it State is the State (a part of) your application is in. States? “Logged in/Logged out” is a state. “Saved/Unsaved” is a state. “Changed/Unchanged” is a state. “Going up/down/left/right” is a state.
  5. Do some refactorings on chains of If/Then/Else statements – And see if a State Pattern emerges. Verify it with the given example is clear and if you can find better ways to do it than I did.
  6. Verify my refactoring to a State Pattern – Using “Design Patterns for Dummies” and “Design Patterns”. See if I am right or wrong.
Advertisements