Design Patterns: State

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • weaknessforcats
    Recognized Expert Expert
    • Mar 2007
    • 9214

    Design Patterns: State

    Design Patterns – State

    Often computer software operates based on a condition called a state. These states traditionally have been implemented using a switch statement. The cases of the switch represent the various states. The example below might be used to read a disc file

    [code=cpp]
    int state = 1;
    switch (state)
    {
    case 1:
    //open the file
    state = 2;
    break;
    case 2;
    //read a record from the file
    if end of file
    {
    state = 4;
    break;
    }
    state =3;
    break;
    ......case 3:
    ........ //displays the record
    state = 2;
    break;
    case 4:
    //close the file
    state = 0;
    break;
    }
    [/code]
    In the above switch the user sets the initial state to open the file. Once the file is opened, the state variable is changed to the state for reading the file. The read state checks for end of file and if the end of file is not reached the state is changed to the display state. The display state displays the record and then sets the state back to the read state to read the file for the next record. In the read state, if the end of file was reached, the state is changed to the close the file. When the file is closed, the state is changed to no-state and the switch is exited.

    Note that all that is required is that each case (state) has to know the following state.

    Here is the above switch used to read a text file from the disc. Assume the disc file contains:

    This is the
    easiest thing I have
    ever seen

    The code would be:
    [code=cpp]
    int main()
    {
    fstream input;
    int state = 1;
    char buffer[256];
    while (state)
    {
    switch (state)
    {
    case 1:
    //open the file
    input.open("c:\ \scratch\\instr uctor\\input.tx t", ios::in);
    state = 2;
    break;
    case 2:
    //read a record from the file
    input.getline(b uffer, 256);
    if (input.eof() == true)
    {
    state = 4;
    break;
    }
    state = 3;\
    break;
    case 3:
    //displays the record
    cout << buffer << endl;
    state = 2;
    break;
    case 4:
    //close the file
    input.close();
    state = 0;
    break;
    } //end of switch
    } //end of while
    }
    [/code]

    The user sets the initial state and then writes a loop that continues as long as a state (a case in the switch statement) exists. Each case performs its action and then changes the state to the successor state.

    Try this code yourself. Compile and run it until you see how the cases work together.

    Limitations
    The limitations of the switch method (a form of go-to programming) are that the switch needs to be changed if any states need to be added or removed. Further, the user of the switch needs to know about the states. That is, there is no context. Everything is done right up front with no encapsulation.

    With a large base of installed software, changing the switch will result is a requirement to re-install the user base. That is, the ripple caused by the change has the proportions of a tsunami.

    The Object-Oriented Approach – Part 1

    What really happens with the switch is that the processing changes based on the state. In the object-oriented world a change of behavior implies a different type of object. In this case, what is needed is an object that appears to change its type based on its internal state.

    This object is called the context object and it contains the state, and the request for what to do. The user merely creates the context object and passes in a request for service.

    To illustrate this, the example below will implement the switch in an object-oriented manner.

    [code=cpp]
    int main()
    {
    FileRead context;
    context.Request ("c:\\scratch\\ instructor\\inp ut.txt");
    context.Process ();

    } //end of main
    [/code]
    First, a context object is created followed by submitting a request for service. In this case, the request is the name of the disc file to read. Then, the context object is asked to process the request. The result will be the disc records displayed on the screen.

    There is nothing here about states. The user is freed from this burden.

    The context class is:
    [code=cpp]
    class FileRead
    {
    private:
    State* CurrentState;
    fstream TheFile;
    string request;
    public:
    FileRead();
    ~FileRead();
    void Request(string name);
    void Process();
    void ChangeState(Sta te* theNewState);
    fstream& GetFileStream() ;
    };
    [/code]
    The context object contains its internal state as CurrentState. It is this member that needs to change type as the context object processes a request. Since this is a file reading context object, it also contains the file stream and a request which is used to pass information to the CurrentState. The meaning of this request will vary based on the type of the CurrentState. There is a method to receive a request for service and a method to process that request.There is a method to change the internal state by changing the type of object pointed at by the CurrentState. Finally, there is a method to return a reference to the file stream.

    The implementation of these methods might be as follows:
    [code=cpp]
    FileRead::FileR ead() : CurrentState(0)
    {

    }
    void FileRead::Proce ss()
    {
    ChangeState(new OpenState);
    string token;
    while (CurrentState)
    {
    CurrentState->DoProcess(th is->request, *this, token);
    }
    }
    void FileRead::Chang eState(State* theNewState)
    {
    delete CurrentState;
    CurrentState = theNewState;
    }
    fstream& FileRead::GetFi leStream()
    {
    return this->TheFile;
    }
    void FileRead::Reque st(string name)
    {
    this->request = name;
    }
    FileRead::~File Read()
    {
    delete CurrentState;
    }
    [/code]
    The actual states are a polymorphic hierarchy with State as the base class. The State class has a virtual destructor signaling that it can be used as a polymorphic base class. The only method is a process method that receives the request, the context object, and the token.
    [code=cpp]
    class State
    {
    public:
    virtual ~State(); //a signal that it's OK to derive from this class.

    //the derived object's process
    virtual void DoProcess(strin g& request, FileRead& context, string& token) = 0;
    };
    State::~State()
    {
    //nothing to do
    }

    [/code]

    These are the derived classes for the various states. The DoProcess method is an override of the State base class DoProcess method. The override is private in the derived classes forcing the call to come from a State pointer or reference. These derived states are not to be used directly.

    There is a token argument that can be used as needed to pass information from state to state as the request is processed.
    [code=cpp]
    class OpenState :public State
    {
    private:
    virtual void DoProcess(strin g& request, FileRead& context, string& token);

    };
    class ReadState :public State
    {
    private:
    virtual void DoProcess(strin g& request, FileRead& context, string& token);

    };
    class DisplayState :public State
    {
    private:
    virtual void DoProcess(strin g& request, FileRead& context, string& token);

    };
    class ExitState :public State
    {
    private:
    virtual void DoProcess(strin g& request, FileRead& context, string& token);

    };
    [/code]
    These are the DoProcess implementations of the various states. You can see how the request varies in its meaning based on the state. OpenState sees the request as the file name to open. ReadState sees it as a way to return the record read.
    [code=cpp]

    void OpenState::DoPr ocess(string& request, FileRead& context, string& token)
    {
    context.GetFile Stream().open(r equest.c_str(), ios::in);
    context.ChangeS tate(new ReadState);
    }
    void ReadState::DoPr ocess(string& request, FileRead& context, string& token)
    {
    char buffer[256];
    context.GetFile Stream().getlin e(buffer, 256);
    if (context.GetFil eStream().eof() == true)
    {
    context.ChangeS tate(new ExitState);
    return;
    }
    request = buffer;
    token = buffer;
    context.ChangeS tate(new DisplayState);

    }
    void DisplayState::D oProcess(string & request, FileRead& context, string& token)
    {
    if (token.size())
    {
    cout << token << endl;
    }
    context.ChangeS tate(new ReadState);
    }
    void ExitState::DoPr ocess(string& request, FileRead& context, string& token)
    {
    context.GetFile Stream().close( );
    request.erase() ;
    token.erase();
    context.ChangeS tate(0);
    }
    [/code]

    You should be able to assemble these code samples and be able to read and display any text file with records of 256 bytes or less.

    With a little improvement, the 256 byte limit could be removed. Then states could be added to write to the file. The display could be removed. The final product would be an object that could read and write any file.

    The Object-Oriented Approach – Part 2
    As often happens, requirements change. The above example displays the records read from the file with one record per line. The new requirement asks that each individual word of each record be displayed on a separate line.

    To accommodate this requirement, some new states need to be added. In addition, existing states may have new successor states.

    To process a record into words, two states have been added. One, the WhiteSpace state, removes leading whitespace characters. The second, the WordState, breaks a word off the record and stores in the token.

    These new classes look like:
    [code=cpp]
    class WhiteSpaceState :public State
    {
    private:
    virtual void DoProcess(strin g& request, FileRead& context, string& token);

    };
    class WordState :public State
    {
    private:
    virtual void DoProcess(strin g& request, FileRead& context, string& token);

    };
    [/code]

    The DoProcess methods look like:
    [code=cpp]

    void WhiteSpaceState ::DoProcess(str ing& request, FileRead& context, string& token)
    {
    while (isspace(reques t[0]) )
    {
    request.erase(0 ,1);
    }

    if (request.size() )
    {
    context.ChangeS tate(new WordState);
    }
    else
    {
    context.ChangeS tate(new ReadState);
    return;
    }


    }

    void WordState::DoPr ocess(string& request, FileRead& context, string& token)
    {
    token.erase();
    while (request.size() && !isspace(reques t[0]))
    {
    token +=request[0];
    request.erase(0 , 1);
    }

    //This is the successor state
    context.ChangeS tate (new DisplayState);
    }
    [/code]
    The WhiteSpaceState ::DoProcess determines if there is a request. If not, the state is changed to the ReadState. Otherwise, leading whitespace characters are removed from the request and the state is changed to the WordState.

    The WordState::DoPr ocess will place the word in the token. It simply does a character-by-character move until it encounters a whitespace character. With the word in the token, the state is changed to the DisplayState so the word can be displayed on the screen.

    To support these new states, some existing states now have new successor states. Shown below are the DoProcess methods that need to be changed. ReadState::DoPr ocess now has a WhiteSpaceState successor so the record (the request) and be processed into words (tokens). DisplayState::D oProcess also has a successor state of WhiteSpaceState to get the next token.

    Shown below are the methods from the first example with the old states commented out and the new states added.

    [code=cpp]
    void ReadState::DoPr ocess(string& request, FileRead& context, string& token)
    {
    char buffer[256];
    context.GetFile Stream().getlin e(buffer, 256);
    if (context.GetFil eStream().eof() == true)
    {
    context.ChangeS tate(new ExitState);
    return;
    }
    request = buffer;
    token = buffer;
    context.ChangeS tate(new WhiteSpaceState );
    //context.ChangeS tate(new DisplayState);

    }
    void DisplayState::D oProcess(string & request, FileRead& context, string& token)
    {
    if (token.size())
    {
    cout << token << endl;
    }
    context.ChangeS tate(new WhiteSpaceState );
    //context.ChangeS tate(new ReadState);

    }
    [/code]

    Notice that in adding these states, there was no change to the FileRead class at all. That means there was no change in main() either. Besides that, the only changes in the existing states were to identify new successor states. The new word breakout capability was accomplished by changing only two lines of code in the original application. The rest of the word breakout requirement was implemented with new code.

    By hiding the details of the various states in derived classes, a high degree of encapsulation is achieved. This reduces the ripple caused by a change.

    Using Handles
    This examples in this article use pointers since pointer syntax is commonly understood. However, it is recommended in a real application that handles be used. You should refer to the article on Handles in the C/C++ Articles section.

    Further Information
    Refer to the book Design Patterns by Erich Fromm, et al, Addison-Wesley 1994.

    This article shows only the conceptual basis of the State pattern but not motivations and ramifications of using this pattern.

    Copyright 2007 Buchmiller Technical Associates North Bend WA USA
    Last edited by weaknessforcats; Mar 4 '08, 06:32 PM. Reason: Changed Logic flow in WhiteSpaceState::DoProcess
  • Banfa
    Recognized Expert Expert
    • Feb 2006
    • 9067

    #2
    I think there is an inefficiency in WhiteSpaceState ::DoProcess. If checks request and selects a new state and then processes request. This means if the line ends in white space that the code has to go through the states

    WordState
    DisplayState
    ReadState

    to consume the white space and read the next line.

    If WhiteSpaceState ::DoProcess processed request and then selected it's state it would proceed directly to ReadState without going through the intervening states.

    I am however finding the article very useful thank you

    Comment

    • weaknessforcats
      Recognized Expert Expert
      • Mar 2007
      • 9214

      #3
      Yes, but the idea of a State machine is that each state only knows the following state. Therefore, WhiteSpaceState only knows to hand control to WordState. It doesn't know about the other states. There is no uber view. True, some extra states get processed if the line ends in whitespace.

      But, on the other hand, the state machine is relentless and the logic steps are finite. This allows you to change the machine and add or remove states. And if ReadState goes away and is replaced by something other state(s), WhiteSpaceState doesn't ned to know about that.

      Comment

      • Banfa
        Recognized Expert Expert
        • Feb 2006
        • 9067

        #4
        This argument would hold water if WhitesSpaceStat e did not already transition to ReadState in some circumstances, I am merely suggesting altering the order of the already existing code and logic in WhiteSpaceState not adding to it :D

        Comment

        • weaknessforcats
          Recognized Expert Expert
          • Mar 2007
          • 9214

          #5
          I agree. I have altered the WhitespaceState ::DoProcess to process the request and then select the follow-on state.

          Comment

          Working...