Problem the State Pattern Tries to Solve:
The State pattern is closely related to the concept of a Finite-State Machine.
The main idea is that, at any given moment, there’s a finite
number of states which a program can be in. Within any unique
state, the program behaves differently, and the program can
be switched from one state to another instantaneously. However, depending on a current state, the program may or may
not switch to certain other states. These switching rules, called
transitions, are also finite and predetermined.
The State pattern is designed to solve the problem of managing an object's behavior when its internal state changes. Often, objects may behave differently depending on their current state, leading to complex conditional logic (e.g., if-else
or switch
statements) scattered throughout the code. This can make the code difficult to maintain, extend, and understand. The State pattern encapsulates state-specific behavior into separate classes, allowing the object to change its behavior dynamically as its state changes.
Key Issues Addressed by the State Pattern:
-
Complex Conditional Logic: Instead of having numerous
if-else
statements within a class to handle different states, the State pattern delegates this logic to state-specific classes. - Code Maintainability: By encapsulating state-specific behavior in different classes, the code becomes more modular and easier to maintain.
- Scalability: Adding new states becomes easier as you can simply create new state classes without modifying the existing code.
- Cleaner Design: The pattern promotes a cleaner design by adhering to the Single Responsibility Principle, where each class has a single responsibility related to a specific state.
Solution
The State pattern suggests that you create new classes for
all possible states of an object and extract all state-specific
behaviors into these classes.
Instead of implementing all behaviors on its own, the original object, called context, stores a reference to one of the state
objects that represent its current state, and delegates all the
state-related work to that object.
Example: Vending Machine
Scenario:
Consider a vending machine that can be in one of several states: accepting money, dispensing product, or out of service. Depending on its state, the machine will behave differently when a user interacts with it (e.g., inserting money, selecting a product, or requesting a refund). Without the State pattern, you might end up with a class filled with conditional logic to handle these states.
To transition the context into another state, replace the active
state object with another object that represents that new state.
This is possible only if all state classes follow the same interface and the context itself works with these objects through
that interface.
This structure may look similar to the Strategy pattern, but
there’s one key difference. In the State pattern, the particular
states may be aware of each other and initiate transitions from
one state to another, whereas strategies almost never know
about each other.
Without State Pattern:
public class VendingMachine {
public enum State { AcceptingMoney, DispensingProduct, OutOfService }
private State currentState;
private decimal balance;
public void InsertMoney(decimal amount) {
if (currentState == State.AcceptingMoney) {
balance += amount;
Console.WriteLine("Money accepted");
} else if (currentState == State.OutOfService) {
Console.WriteLine("Machine is out of service");
}
}
public void SelectProduct() {
if (currentState == State.AcceptingMoney && balance >= 1.00M) {
currentState = State.DispensingProduct;
DispenseProduct();
} else {
Console.WriteLine("Not enough balance or machine not accepting money");
}
}
private void DispenseProduct() {
if (currentState == State.DispensingProduct) {
Console.WriteLine("Product dispensed");
currentState = State.AcceptingMoney;
}
}
}
-
Problem: The
VendingMachine
class is tightly coupled with the different states and behavior through conditional logic. If you need to add or modify a state, you must modify the entire class, making the code difficult to manage.
With State Pattern:
Now, let's refactor the design using the State pattern.
// Abstract State class
public abstract class VendingMachineState {
public abstract void InsertMoney(VendingMachine machine, decimal amount);
public abstract void SelectProduct(VendingMachine machine);
}
// AcceptingMoney state
public class AcceptingMoneyState : VendingMachineState {
public override void InsertMoney(VendingMachine machine, decimal amount) {
machine.Balance += amount;
Console.WriteLine("Money accepted");
}
public override void SelectProduct(VendingMachine machine) {
if (machine.Balance >= 1.00M) {
machine.SetState(new DispensingProductState());
machine.DispenseProduct();
} else {
Console.WriteLine("Not enough balance");
}
}
}
// DispensingProduct state
public class DispensingProductState : VendingMachineState {
public override void InsertMoney(VendingMachine machine, decimal amount) {
Console.WriteLine("Wait, product is being dispensed");
}
public override void SelectProduct(VendingMachine machine) {
Console.WriteLine("Wait, product is being dispensed");
}
}
// OutOfService state
public class OutOfServiceState : VendingMachineState {
public override void InsertMoney(VendingMachine machine, decimal amount) {
Console.WriteLine("Machine is out of service");
}
public override void SelectProduct(VendingMachine machine) {
Console.WriteLine("Machine is out of service");
}
}
// Context class
public class VendingMachine {
private VendingMachineState currentState;
public decimal Balance { get; set; }
public VendingMachine() {
currentState = new AcceptingMoneyState();
}
public void SetState(VendingMachineState state) {
currentState = state;
}
public void InsertMoney(decimal amount) {
currentState.InsertMoney(this, amount);
}
public void SelectProduct() {
currentState.SelectProduct(this);
}
public void DispenseProduct() {
Console.WriteLine("Product dispensed");
Balance -= 1.00M;
SetState(new AcceptingMoneyState());
}
}
Benefits of Using the State Pattern:
-
Encapsulation of State Behavior: Each state-related behavior is encapsulated in its own class (
AcceptingMoneyState
,DispensingProductState
,OutOfServiceState
), which makes the code modular and easier to maintain. -
Simplified VendingMachine Class: The
VendingMachine
class itself is now simpler and delegates behavior to the current state object, making it easier to extend or modify states without changing the context class. -
Scalability: If you need to add a new state (e.g.,
MaintenanceState
), you simply create a new state class without touching the existing code. -
Clarity: The design is clearer, as each state handles its own logic, and there’s no need for complex
if-else
chains within theVendingMachine
class.
Applicability
Use the State pattern when you have an object that behaves
differently depending on its current state, the number of states
is enormous, and the state-specific code changes frequently.
The pattern suggests that you extract all state-specific code
into a set of distinct classes. As a result, you can add new
states or change existing ones independently of each other,
reducing the maintenance cost.
Use the pattern when you have a class polluted with massive
conditionals that alter how the class behaves according to the
current values of the class’s fields.
The State pattern lets you extract branches of these conditionals into methods of corresponding state classes. While doing
so, you can also clean temporary fields and helper methods
involved in state-specific code out of your main class.
Use State when you have a lot of duplicate code across similar
states and transitions of a condition-based state machine.
The State pattern lets you compose hierarchies of state classes and reduce duplication by extracting common code into
abstract base classes.
Conclusion:
The State pattern helps manage state-specific behavior by delegating responsibilities to different state classes. It makes the code more modular, scalable, and easier to maintain, particularly in scenarios where an object's behavior is heavily dependent on its current state. By adhering to this pattern, you can avoid complex conditional logic and create a cleaner, more maintainable design.
Top comments (0)