Skip to main content

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.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

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 DETAILS

What 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.

YouTube @cloudaffle