Real World Implementation Of The State Design Pattern
Consider a document editing application that has several different tool states ( e.g., Select tool, Brush tool, Eraser tool). Depending on the current tool state, user interactions with the canvas will have different effects.
Here is the class diagram for the document editing tool application using the mermaid markdown syntax:
In this diagram, Canvas
has a reference to Tool
(marked with "1" --> "*"
)
and SelectionTool
, BrushTool
, and EraserTool
implement the Tool
interface (marked with ..|>
). The -tool: Tool
notation inside Canvas
indicates a private field, while
the +setTool(tool: Tool): void
, +onMouseDown(): void
,
and +onMouseUp(): void
denote public methods.
Implementation Of the State Pattern
Here's how this could be modeled using the state pattern:
interface Tool {
onMouseDown(): void;
onMouseUp(): void;
}
class SelectionTool implements Tool {
onMouseDown(): void {
console.log("Selection rectangle started.");
}
onMouseUp(): void {
console.log("Selection rectangle drawn.");
}
}
class BrushTool implements Tool {
onMouseDown(): void {
console.log("Brush stroke started.");
}
onMouseUp(): void {
console.log("Brush stroke drawn.");
}
}
class EraserTool implements Tool {
onMouseDown(): void {
console.log("Eraser started.");
}
onMouseUp(): void {
console.log("Erased.");
}
}
class Canvas {
private tool: Tool;
constructor(tool: Tool) {
this.tool = tool;
}
setTool(tool: Tool) {
this.tool = tool;
}
onMouseDown() {
this.tool.onMouseDown();
}
onMouseUp() {
this.tool.onMouseUp();
}
}
// Usage
let canvas = new Canvas(new SelectionTool());
canvas.onMouseDown(); // Selection rectangle started.
canvas.onMouseUp(); // Selection rectangle drawn.
canvas.setTool(new BrushTool());
canvas.onMouseDown(); // Brush stroke started.
canvas.onMouseUp(); // Brush stroke drawn.
canvas.setTool(new EraserTool());
canvas.onMouseDown(); // Eraser started.
canvas.onMouseUp(); // Erased.
In this example, Canvas
is the context, and Tool
is the state
interface. SelectionTool
, BrushTool
, and EraserTool
are concrete state
classes. When we call canvas.onMouseDown()
or canvas.onMouseUp()
, it
delegates the request to the current tool state object to handle it. The tool
state object processes the request, providing the appropriate behavior for the
current state.
Advantages Of The State Design Pattern
The State pattern offers several advantages, which I'll illustrate with references to the code above.
Single Responsibility Principle
Each state class has its own set of behaviors, thus adhering to the Single Responsibility Principle.
class SelectionTool implements Tool {
onMouseDown(): void {
console.log("Selection rectangle started.");
}
onMouseUp(): void {
console.log("Selection rectangle drawn.");
}
}
In the above code, SelectionTool
is only concerned with selection behaviors,
and it doesn't need to be concerned with any other behaviors (like brush stroke
or eraser behaviors).
Open/Closed Principle
New states can be added without changing the existing code, thus adhering to the Open/Closed Principle.
class EraserTool implements Tool {
onMouseDown(): void {
console.log("Eraser started.");
}
onMouseUp(): void {
console.log("Erased.");
}
}
In the above code, the EraserTool
is an example of a new state that can be
added without altering the existing states or the Canvas
class.
Simplifies Complex State Logic
The complexity of managing state transitions and behavior is distributed across multiple state classes rather than being packed into one large method or class.
class Canvas {
private tool: Tool;
setTool(tool: Tool) {
this.tool = tool;
}
}
In the Canvas
class, the setTool()
method shows that switching states is as
simple as assigning a new state to tool
. This greatly simplifies state
transition logic.
Encapsulates State-Specific Behavior
The State pattern encapsulates state-specific behavior in individual state classes. This improves code organization and makes it easier to understand and maintain.
class BrushTool implements Tool {
onMouseDown(): void {
console.log("Brush stroke started.");
}
onMouseUp(): void {
console.log("Brush stroke drawn.");
}
}
In the above code, BrushTool
encapsulates all the behaviors related to the
brush tool. If we need to modify or understand brush-related behaviors, we just
need to look at this class.
Dynamic State Transitions
The State pattern allows an object to alter its behavior when its internal state changes. This transition can be done at runtime.
let canvas = new Canvas(new SelectionTool());
canvas.onMouseDown(); // Selection rectangle started.
canvas.onMouseUp(); // Selection rectangle drawn.
canvas.setTool(new BrushTool());
canvas.onMouseDown(); // Brush stroke started.
canvas.onMouseUp(); // Brush stroke drawn.
canvas.setTool(new EraserTool());
canvas.onMouseDown(); // Eraser started.
canvas.onMouseUp(); // Erased.
In the above usage example, the canvas
object can change its state dynamically
at runtime using the setTool()
method. Depending on its current state, it
exhibits different behaviors.
Time To Transition From JavaScript To TypeScript
Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.
SEE COURSE DETAILSWhat Can You Do Next 🙏😊
If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for software developers.