State Pattern
Introduction
The State pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. This pattern is akin to having a set of different classes for each state of an object, with each class implementing behavior for that specific state.
Purpose
- To allow an object to change its behavior when its internal state changes.
- To encapsulate varying behavior for the same routine based on an object’s internal state.
Use Cases
- When an object’s behavior depends on its state, and it must change its behavior at runtime depending on that state.
- When operations have large, multipart conditional statements that depend on the object’s state. This state-specific logic can be moved into separate classes.
Advantages/Disadvantages
Advantages:
- Organizes state-specific code into separate classes.
- Simplifies complex conditional statements in the context class.
- Makes adding new states easier as the system evolves.
Disadvantages:
- Can increase the number of classes, which may complicate maintenance.
- If not managed well, it can lead to duplication between similar states.
Implementation in JavaScript
class State {
handle(context) {}
}
class ConcreteStateA extends State {
handle(context) {
console.log('Handling state A.');
context.setState(new ConcreteStateB());
}
}
class ConcreteStateB extends State {
handle(context) {
console.log('Handling state B.');
context.setState(new ConcreteStateA());
}
}
class Context {
constructor() {
this.state = new ConcreteStateA();
}
setState(state) {
this.state = state;
}
request() {
this.state.handle(this);
}
}
// Usage
const context = new Context();
context.request(); // Handling state A
context.request(); // Handling state B
Practical Example
Consider a backend application where you’re implementing a ticket tracking system. The ticket can be in different states (like ‘new’, ‘assigned’, ‘closed’) and the behavior changes based on its current state.
class TicketState {
next(ticket) {}
prev(ticket) {}
printStatus() {}
}
class NewState extends TicketState {
next(ticket) {
ticket.setState(new AssignedState());
}
printStatus() {
console.log('Ticket in New state.');
}
}
class AssignedState extends TicketState {
next(ticket) {
ticket.setState(new ClosedState());
}
prev(ticket) {
ticket.setState(new NewState());
}
printStatus() {
console.log('Ticket in Assigned state.');
}
}
class ClosedState extends TicketState {
prev(ticket) {
ticket.setState(new AssignedState());
}
printStatus() {
console.log('Ticket in Closed state.');
}
}
class Ticket {
constructor() {
this.state = new NewState();
}
setState(state) {
this.state = state;
}
next() {
this.state.next(this);
}
prev() {
this.state.prev(this);
}
printStatus() {
this.state.printStatus();
}
}
// Usage
const ticket = new Ticket();
ticket.printStatus(); // New state
ticket.next();
ticket.printStatus(); // Assigned state
ticket.next();
ticket.printStatus(); // Closed state
ticket.prev();
ticket.printStatus(); // Assigned state
In this example, Ticket
changes its behavior based on its state, represented by NewState
, AssignedState
, and ClosedState
. Each state has different implementations for the next
, prev
, and printStatus
methods.