Nice write-up! Curious—how would you handle state changes that depend on external data?
It doesn't matter if you have rules for changing the state or if the source of the change is external. What matters is that you have a rule for this change.
Then you can treat it as a "state" that will implement the state functions interface that represent the rules, receive this data from the outside and change the object's state to a new state.
But if you don't have rules, then it is better to use another pattern, such as "chain of responsibility" or "observer."
Nice writeup Hussein,
I think that for the different states of Student, it may be a better idea to use enum instead of using int
and creating a new object each time for the state.
The following code shows how an enum could be more useful:
class Student {
enum StudentStateEnum implements StudentState {
PREPARING("Preparing for exam") {
@Override
public void takeExam(Student student) {
student.setState(EXAM_TAKEN);
}
},
EXAM_TAKEN("Waiting for results") {
@Override
public void receiveResults(Student student, boolean passed) {
student.setState(passed ? PASSED : PREPARING);
}
},
PASSED("Passed the exam"),
;
private String name;
StudentStateEnum(String n) {
this.name = name;
}
@Override
public String getStateName() {
return this.name;
}
@Override
public void study(Student student) {
throw new IllegalStateException("invalid state");
}
@Override
public void takeExam(Student student) {
throw new IllegalStateException("invalid state");
}
@Override
public void receiveResults(Student student, boolean passed) {
throw new IllegalStateException("invalid state");
}
}
private StudentState state = StudentStateEnum.PREPARING;
private String name;
public Student(String name) {
this.name = name;
}
public void setState(StudentState state) {
this.state = state;
}
public void study() {
System.out.println(name + " is attempting to study.");
state.study(this);
}
public void takeExam() {
System.out.println(name + " is attempting to take the exam.");
state.takeExam(this);
}
public void receiveResults(boolean passed) {
System.out.println(name + " is receiving exam results.");
state.receiveResults(this, passed);
}
public String getStateName() {
return state.getStateName();
}
@Override
public String toString() {
return name + " - Current state: " + state.getStateName();
}
}
Yes, exactly, and this solution is correct, but in one case: "if the state is abstract." That is, if the object doesn't have its own functions and is merely a transit condition, then your approach works. However, if it has properties and functions specific to that object, it's better to separate it and make it an object.
Also, your reference to Int is agreed upon as the worst solution, and I presented it as the worst solution before the existence of this pattern. However, after its discovery, objects began to be used.
Note that an enum is a type of class and has many built-in features that support reliability and address the problem of reflection. However, it's best to use it when needed, depending on the project's specific situation.
Here is another version of the state.
Simplifies maintenance by encapsulating behavior per state, reducing switch-case clutter, and making it easy to add new states .
I hope you guys like it. C# version.
public interface ICoffeeMachineState
{
void InsertCoin();
void SelectBeverage(string beverage);
void Dispense();
}
public class IdleState : ICoffeeMachineState
{
private readonly CoffeeMachine _machine;
public IdleState(CoffeeMachine machine) => _machine = machine;
public void InsertCoin()
{
Console.WriteLine("Coin inserted. Ready to select beverage.");
_machine.SetState(_machine.SelectionState);
}
public void SelectBeverage(string beverage)
{
Console.WriteLine("Please insert a coin first.");
}
public void Dispense()
{
Console.WriteLine("Insert coin and select beverage first.");
}
}
public class SelectionState : ICoffeeMachineState
{
private readonly CoffeeMachine _machine;
public SelectionState(CoffeeMachine machine) => _machine = machine;
public void InsertCoin()
{
Console.WriteLine("Coin already inserted.");
}
public void SelectBeverage(string beverage)
{
Console.WriteLine($"{beverage} selected. Dispensing now...");
_machine.SetState(_machine.DispensingState);
_machine.Dispense();
}
public void Dispense()
{
Console.WriteLine("Please select a beverage first.");
}
}
public class DispensingState : ICoffeeMachineState
{
private readonly CoffeeMachine _machine;
public DispensingState(CoffeeMachine machine) => _machine = machine;
public void InsertCoin()
{
Console.WriteLine("Please wait, dispensing in progress.");
}
public void SelectBeverage(string beverage)
{
Console.WriteLine("Already dispensing. Please wait.");
}
public void Dispense()
{
Console.WriteLine("Here is your coffee. Thank you!");
_machine.SetState(_machine.IdleState);
}
}
public class CoffeeMachine
{
public ICoffeeMachineState IdleState { get; }
public ICoffeeMachineState SelectionState { get; }
public ICoffeeMachineState DispensingState { get; }
private ICoffeeMachineState _currentState;
public CoffeeMachine()
{
IdleState = new IdleState(this);
SelectionState = new SelectionState(this);
DispensingState = new DispensingState(this);
_currentState = IdleState;
}
public void SetState(ICoffeeMachineState state) => _currentState = state;
public void InsertCoin() => _currentState.InsertCoin();
public void SelectBeverage(string beverage) => _currentState.SelectBeverage(beverage);
public void Dispense() => _currentState.Dispense();
}
var machine = new CoffeeMachine();
machine.SelectBeverage("Espresso"); // Error: insert coin first
machine.InsertCoin(); // Moves to selection
machine.SelectBeverage("Espresso"); // Moves to dispensing
machine.InsertCoin(); // Wait until dispensing finishes