iTranslated by AI
[TS] Understanding the State Pattern: A Back-to-Basics Guide
Introduction
In this article, I will explain the State Pattern.
It is a design pattern that uses polymorphism to represent behaviors for each state.
What is the State Pattern?
There is a well-known explanation by TECHSCORE.
- Reference: TECHSCORE | 19. State Pattern
The following is a quote:
Patterns of behavior often change depending on the state. For example, consider asking a mother, who can be in two states: "in a good mood" and "in a bad mood," to do a few things. If you ask a mother in a good mood for an "allowance" or "snacks," she might say "sure, sure" and give you money or snacks. However, if you make these requests to a mother in a bad mood, she might not listen to them. The mother's behavior changes depending on her state.
The State Pattern is a pattern that proves powerful in such cases where behavior changes according to changes in state.
Example
Consider the following "Dog" class.
- It has states: "Hungry" and "Full".
- It has a "play together" function; when "Full", it is in a good mood and plays, but when "Hungry", it gets angry and asks for food.
- It has an "eat food" function; when "Hungry", it eats, but when "Full", it does not eat.
Implementation Without State Pattern
I think coding the example without being particularly conscious of any design pattern would look like this:
class Dog {
// State
private _status: 'Hunger' | 'Full'
constructor(status: 'Hunger' | 'Full' ) {
this.setStatus(status);
}
public setStatus(status: 'Hunger' | 'Full') {
this._status = status;
}
// Play together
public withPlay(): void {
switch(this._status) {
case 'Hunger':
console.log("I'm hungry, give me food!");
break;
case 'Full':
console.log("Let's play!");
break;
}
}
// Eat food
public eat(): void {
switch(this._status) {
case 'Hunger':
console.log("Yay! Food!");
break;
case 'Full':
console.log("I'm already full...");
break;
}
}
}
In each function, the state is determined using a switch statement to execute the appropriate processing.
Since there are only two types of states, it's not much of a problem, but if you were to "add more states" or "increase the number of functions involving states" from here, you would have to review the branching logic every time.
Implementation with State Pattern
This is where the State Pattern comes in.
In the State Pattern, "state-related functions" are defined as an interface, and then each state implements them.
The side that holds the state (the Dog class in this example) simply swaps the state object.
// Stomach state interface
interface StomachState {
withPlay():void;
eat():void;
}
// Hungry state
class HungerState implements StomachState {
withPlay = () => console.log("I'm hungry, give me food!");
eat = () => console.log("Yay! Food!");
}
// Full state
class FullState implements StomachState {
withPlay = () => console.log("Let's play!");
eat = () => console.log("I'm already full...");
}
class Dog {
private _state: StomachState;
constructor(state: StomachState) {
this.setStatus(state);
}
public setStatus(state: StomachState) {
this._state = state;
}
// Since withPlay and eat are defined on the _state side,
// it just executes them without being conscious of their content.
withPlay = () => this._state.withPlay();
eat = () => this._state.eat();
}
The key points are that "the Dog class is no longer conscious of its own state internally" and "changes or additions to a state do not affect the processing of other states."
Since HungerState and FullState implement StomachState as "stomach states," if a state like "half-full" were to be added, you would just need to implement a new StomachState.
This means that bugs where the behavior of FullState or StomachState accidentally changes will not occur (if you had written it heavily with switch statements, there was room for omissions or unintended changes).
Difference from Strategy Pattern
In this article, I introduced the "State Pattern," which is one of the design patterns in TypeScript.
The State Pattern is often said to be similar to the "Strategy Pattern."
This is because the final code for each pattern when implemented is almost the same.
The following is my personal view.
While the State Pattern takes the approach of "solving state-based branching with polymorphism," the Strategy Pattern takes the approach of "solving the extraction of algorithms with polymorphism."
The point is that "the only difference is the focus; the solution using polymorphism is the same."
Therefore, "the starting philosophy is different, but the appearance when translated into code is the same."
I have summarized the Strategy Pattern in the following article:
Discussion