State - kgleong/software-engineering GitHub Wiki
Motivation
The State design pattern allows an entity to neatly encapsulate state-based behavior.
A naive approach is to establish an Enum
to represent the current state of an object, and vary behavior based on the state via conditionals or switch statements.
This can easily become messy and become unmaintainable.
Also, having separate classes creates well-defined states, rather than relying on an Enum
member variable that conditional statements examine.
Instead, the State pattern establishes different classes to represent each state.
The entity, referred to as the context, contains a reference to an object that implements a State
interface, which changes classes as the context moves through its lifecycle.
Code
public class Context {
State mState;
public void handleRequest() {
System.out.println("Request received by Context.");
mState.handle(this);
}
public void setState(State state) {
mState = state;
}
}
External clients send requests via the handleRequest()
method.
The Context
class then delegates the request to its currently associated State
object.
Different State
subclasses are allowed to provide different behavior within their handle()
method.
The example above allows the State
instances to manage state transitions. This decentralizes transition rules and lets each State
object specify its successor State
.
This requires the State
object to be able to change the Context
's state, which requires a setState()
method on the Context
class.
If transition rules are static, it may be warranted to let the Context
manage the state transitions itself.
public abstract class State {
public abstract void handle(Context context);
public static void changeState(Context context, State state) {
context.setState(state);
}
}
The changeState()
method allows a State
to transition a Context
to another state.
public class ConcreteStateA implements State {
@Override
public void handle(Context context) {
System.out.println("ConcreteStateA handling request.");
changeState(context, new ConcreteStateB());
}
}
public class ConcreteStateB implements State {
@Override
public void handle(Context context) {
System.out.println("ConcreteStateB handling request.");
}
}
Above, ConcreteStateA
handles the request, creates the successive State
instances and transitions the Context
.
Creating and destroying State
objects as needed in this manner is one approach.
If State
s are being changed rapidly and reusing objects makes more sense, then it might be more prudent to make State
classes provide Singleton instances.
// Client usage
State initialState = new ConcreteStateA();
Context context = new Context();
context.setState(intialState);
context.handleRequest(); // State change
context.handleRequest();
// Output:
// Request received by Context.
// ConcreteStateA handling request.
// ConcreteStateB handling request.
Use Case
A typical interactive drawing program will allow the user to:
- Select an area on the canvas.
- Draw a line.
- Enter text.
public class DrawingController {
Tool mTool;
public void mousePressed() {
mTool.handleMousePressed();
}
public void mouseReleased() {
mTool.handleMouseReleased();
}
public void characterEntered(Char character) {
mTool.handleCharacter(character);
}
public void setTool(Tool tool) {
mTool = tool;
}
}
The DrawingController
takes in user input and passes it to the currently selected Tool
, which are analogous to State
instances.
Tool
instances are selected by the user via a GUI that calls the setTool()
method on the DrawingController
.
public interface Tool {
void handleMousePressed();
void handleMouseReleased();
void handleCharacter(Char character);
}
Each Tool
represents a different function of the drawing application, allowing each subclass to handle user input differently.
public class SelectionTool implements Tool {
void handleMousePressed() {
// Initialize new selection area.
}
void handleMouseReleased() {
// Calculate selection area and highlight on screen.
}
void handleCharacter(Char character) {
// Change mode. E.g., select rectangle, circle, path.
}
}
public class TextTool implements Tool {
void handleMousePressed() {
// Initialize new text area.
}
void handleMouseReleased() {
// Calculate size of text area and display cursor.
}
void handleCharacter(Char character) {
// Render character to screen at cursor location.
}
}
Each drawing function is contained within a single class, making it easy to dynamically swap Tool
instances in and out.