In their book, Design Patterns, Elements of Reusable Object-Oriented Software, Gamma, Helm, Johnson and Vlissides describe a design pattern called State. The State pattern allows an object to change its behaviour when its internal state changes.
For example, suppose we have a Button class with a (public) member function, Press(). The first time the Press() function is called some piece of equipment is turned on; the second time the equipment is turned off. There is one event (pressing the button) that triggers multiple actions (switch on, switch off).
Gamma, et al. (the Gang of Four), present a design in which each internal state is a separate object. The parent class stores a pointer to the current state object and delegates all operations to the current state. The states are polymorphic. The base class declares a virtual function to handle each event and derived classes provide different implementations of the event handling functions.
class Button; class State { public: virtual void Toggle(Button&); }; class Off : public State { public: virtual void Toggle(Button&); }; class On : public State { public: virtual void Toggle(Button&); }; class Button { public: Button(); void Press() {state->Toggle(*this);} private: State* state; };
Figure 1 - State selects and performs action
The parent class passes a reference to itself to each of the event handling functions so that the current state can be updated. The Design Patterns book makes the State base class a friend and its derived classes call a protected function in State to get access to the state pointer. The implementation must also consider when the state objects are created and destroyed and where they are stored.
This article presents an alternative implementation in which the derived classes, passing a reference to the parent class and friends all become unnecessary. This new implementation tends to be simpler and fits well with the usual solutions to the problems of creating, destroying and accessing the state objects.
In the Gang of Four version the State objects do two things: they select an action and perform that action on behalf of the parent object.
In the new implementation the State objects select an action, but the parent object retains responsibility for performing the selected action.
class Button { public: Button() : state(&off) {} void Press(); private: struct State { void (Button::*toggle)(); }; private: static const State off, on; private: void SwitchOn (); void SwitchOff(); private: const State* state; }; const Button::State Button::off = {&Button::SwitchOn }; const Button::State Button::on = {&Button::SwitchOff}; void Button::Press() {(this->*state->toggle)();}
Figure 2 - State selects action, Button performs action
The State class stores pointers to member functions of the Button class. When a button press event occurs the Press() function uses the current state to get a pointer to a Button member function and executes that function on its own Button object.
If we dissect the Press() function we can break it down into the following steps:
typedef void (Button::*Action)(); // pointer to Button member function void Button::Press() { Action action = state->toggle; // select action (this->*action)(); // perform selected action }
If the current state is 'off', state->toggle points to Button::SwitchOn(), which switches on the equipment and sets the Button state to 'on'. Now, state->toggle points to Button::SwitchOff(), which switches the equipment off again and resets the Button state to 'off'.
void Button::SwitchOn() { // ... switch on the equipment ... state = &on; // change to the 'on' state } void Button::SwitchOff() { // ... switch off the equipment ... state = &off; // change to the 'off' state }
What could be simpler?
The State class only contains data; there are no virtual functions to override and, hence, no need of derived classes. To emphasise this I have used a Plain Old Data type (struct) instead of a class with separate interface and implementation. Purists may prefer to make the data private and provide suitable access functions.
Now that there is just one State class instead of a class hierarchy it can be nested inside the parent class without unduly complicating the parent class declaration. This generates fewer identifiers at namespace scope.
The simple, data-only State objects are good candidates for class scope and static storage. There is no need to use the Singleton design pattern to provide the action functions with access to the State objects or to ensure that all Buttons share the same State information.
The relationships between states, events and actions are fixed at compile time and stored in the State objects, so the State objects can be const qualified. The set of State objects provides a direct implementation of a state table.
This implementation typically produces simpler source code, especially when the parent class contains data that is shared by the action functions. The pointer to the current state is just such a data item. Each action function can change the state of the parent object by directly updating the pointer.
It may be argued that the State class is very weak. However, it is private to Button and, as all of its members are pointers to Button member functions, it is difficult to see how the State representation could change. So, client code can not misuse the State class and little would be gained from hiding the data behind access functions.
The presence of the State class definition within the Button class is slightly more difficult to defend. In large projects this style can lead to excessive dependencies between header files and painfully slow compilations. However, if this is a problem, it can be fixed by storing pointers to the State objects in the Button class instead of the objects themselves. The State class definition can then be moved to the Button's .cpp file, leaving just a declaration of the incomplete type in the header.
The implementation described in Design Patterns uses fully-fledged State objects. Encapsulation, inheritance and polymorphism are all essential to their design. Of course, the Gang of Four were writing a book about object-oriented design patterns in general and pointers to member functions may not be available in other OO languages. In the context of C++, however, I believe applying object-oriented principles too rigidly has led to an inappropriate separation of responsibilities. Like all good things, object-oriented thinking can be overdone.
Overload Journal #33 - Aug 1999 + Design of applications and programs
Browse in : |
All
> Journals
> Overload
> 33
(8)
All > Topics > Design (236) Any of these categories - All of these categories |