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
Expand|Select|Wrap|Line Numbers
- 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;
- }
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:
Expand|Select|Wrap|Line Numbers
- int main()
- {
- fstream input;
- int state = 1;
- char buffer[256];
- while (state)
- {
- switch (state)
- {
- case 1:
- //open the file
- input.open("c:\\scratch\\instructor\\input.txt", ios::in);
- state = 2;
- break;
- case 2:
- //read a record from the file
- input.getline(buffer, 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
- }
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.
Expand|Select|Wrap|Line Numbers
- int main()
- {
- FileRead context;
- context.Request("c:\\scratch\\instructor\\input.txt");
- context.Process();
- } //end of main
There is nothing here about states. The user is freed from this burden.
The context class is:
Expand|Select|Wrap|Line Numbers
- class FileRead
- {
- private:
- State* CurrentState;
- fstream TheFile;
- string request;
- public:
- FileRead();
- ~FileRead();
- void Request(string name);
- void Process();
- void ChangeState(State* theNewState);
- fstream& GetFileStream();
- };
The implementation of these methods might be as follows:
Expand|Select|Wrap|Line Numbers
- FileRead::FileRead() : CurrentState(0)
- {
- }
- void FileRead::Process()
- {
- ChangeState(new OpenState);
- string token;
- while (CurrentState)
- {
- CurrentState->DoProcess(this->request, *this, token);
- }
- }
- void FileRead::ChangeState(State* theNewState)
- {
- delete CurrentState;
- CurrentState = theNewState;
- }
- fstream& FileRead::GetFileStream()
- {
- return this->TheFile;
- }
- void FileRead::Request(string name)
- {
- this->request = name;
- }
- FileRead::~FileRead()
- {
- delete CurrentState;
- }
Expand|Select|Wrap|Line Numbers
- class State
- {
- public:
- virtual ~State(); //a signal that it's OK to derive from this class.
- //the derived object's process
- virtual void DoProcess(string& request, FileRead& context, string& token) = 0;
- };
- State::~State()
- {
- //nothing to do
- }
There is a token argument that can be used as needed to pass information from state to state as the request is processed.
Expand|Select|Wrap|Line Numbers
- class OpenState :public State
- {
- private:
- virtual void DoProcess(string& request, FileRead& context, string& token);
- };
- class ReadState :public State
- {
- private:
- virtual void DoProcess(string& request, FileRead& context, string& token);
- };
- class DisplayState :public State
- {
- private:
- virtual void DoProcess(string& request, FileRead& context, string& token);
- };
- class ExitState :public State
- {
- private:
- virtual void DoProcess(string& request, FileRead& context, string& token);
- };
Expand|Select|Wrap|Line Numbers
- void OpenState::DoProcess(string& request, FileRead& context, string& token)
- {
- context.GetFileStream().open(request.c_str(), ios::in);
- context.ChangeState(new ReadState);
- }
- void ReadState::DoProcess(string& request, FileRead& context, string& token)
- {
- char buffer[256];
- context.GetFileStream().getline(buffer, 256);
- if (context.GetFileStream().eof() == true)
- {
- context.ChangeState(new ExitState);
- return;
- }
- request = buffer;
- token = buffer;
- context.ChangeState(new DisplayState);
- }
- void DisplayState::DoProcess(string& request, FileRead& context, string& token)
- {
- if (token.size())
- {
- cout << token << endl;
- }
- context.ChangeState(new ReadState);
- }
- void ExitState::DoProcess(string& request, FileRead& context, string& token)
- {
- context.GetFileStream().close();
- request.erase();
- token.erase();
- context.ChangeState(0);
- }
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:
Expand|Select|Wrap|Line Numbers
- class WhiteSpaceState :public State
- {
- private:
- virtual void DoProcess(string& request, FileRead& context, string& token);
- };
- class WordState :public State
- {
- private:
- virtual void DoProcess(string& request, FileRead& context, string& token);
- };
Expand|Select|Wrap|Line Numbers
- void WhiteSpaceState::DoProcess(string& request, FileRead& context, string& token)
- {
- while (isspace(request[0]) )
- {
- request.erase(0,1);
- }
- if (request.size())
- {
- context.ChangeState(new WordState);
- }
- else
- {
- context.ChangeState(new ReadState);
- return;
- }
- }
- void WordState::DoProcess(string& request, FileRead& context, string& token)
- {
- token.erase();
- while (request.size() && !isspace(request[0]))
- {
- token +=request[0];
- request.erase(0, 1);
- }
- //This is the successor state
- context.ChangeState (new DisplayState);
- }
The WordState::DoProcess 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::DoProcess now has a WhiteSpaceState successor so the record (the request) and be processed into words (tokens). DisplayState::DoProcess 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.
Expand|Select|Wrap|Line Numbers
- void ReadState::DoProcess(string& request, FileRead& context, string& token)
- {
- char buffer[256];
- context.GetFileStream().getline(buffer, 256);
- if (context.GetFileStream().eof() == true)
- {
- context.ChangeState(new ExitState);
- return;
- }
- request = buffer;
- token = buffer;
- context.ChangeState(new WhiteSpaceState);
- //context.ChangeState(new DisplayState);
- }
- void DisplayState::DoProcess(string& request, FileRead& context, string& token)
- {
- if (token.size())
- {
- cout << token << endl;
- }
- context.ChangeState(new WhiteSpaceState);
- //context.ChangeState(new ReadState);
- }
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