State Pattern

Hyung Jun·2020년 12월 4일
0

Design Pattern

목록 보기
8/10
post-thumbnail

This article is lecture note from "Head First Design Patterns" by E_lisabeth Freeman and Kathy Sierra and CS course from Kookmin by Inkyu Kim

Definition

The State Pattern allows an object to alter its behaviour when its internal state changes. The object will appear to change its class.

Finite-State Machine

It is very reasonable to understand State Pattern relating it to 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.

Intent

We have a gumball machine. Actually not of course.
Imagine we got asked of making gumball machine which has not just selling gumball also has some random game for giving additional gumball.
The original code was like this.

public class GumballMachine {
 
	final static int SOLD_OUT = 0;
	final static int NO_QUARTER = 1;
	final static int HAS_QUARTER = 2;
	final static int SOLD = 3;
 
	int state = SOLD_OUT;
	int count = 0;
  
	public GumballMachine(int count) {
		this.count = count;
		if (count > 0) {
			state = NO_QUARTER;
		}
	}
  
	public void insertQuarter() {
		if (state == HAS_QUARTER) {
			System.out.println("You can't insert another quarter");
		} else if (state == NO_QUARTER) {
			state = HAS_QUARTER;
			System.out.println("You inserted a quarter");
		} else if (state == SOLD_OUT) {
			System.out.println("You can't insert a quarter, the machine is sold out");
		} else if (state == SOLD) {
        	System.out.println("Please wait, we're already giving you a gumball");
		}
	}

	public void ejectQuarter() {
		// ejectQuarter() here
	}
 
	public void turnCrank() {
		// turnCrank() here
	}
 
	private void dispense() {
		// dispense() here
	}
 
	public void refill(int numGumBalls) {
		// refill() here
	}

	public String toString() {
		// toString() here
	}
}

We need to add new STATE which is winner state in random game. Then we need to add a new conditional in every single methods.. well, that's too many code to modify. Right?

Structure

Context stores a reference to one of the concrete state objects and delegates to it all state-specific work. The context communicates with the state object via the state interface. The context exposes a setter for passing it a new state object. The State interface declares the state-specific methods. These methods should make sense for all concrete states because you don't want some of your states to have useless methods that will never be called.
ConcreteStates provide their own implementations for the state-specific methods. To avoid duplication of similar code across multiple states, you may provide intermediate abstract classes that encapsulate some common behaviour. State object may store a backreference to the context object. Through this reference, the state can fetch any required info from the context object, as well as initiate state transitions.
Both context and concrete states can set the next state of the context and perform the actual state transition by replacing the state object linked to the context.

Source Code

First let's create an interface for state, which all our states implement.

<State.java>

public interface State {
 
	public void insertQuarter();
	public void ejectQuarter();
	public void turnCrank();
	public void dispense();
	public void refill();
}

Then we starts with NoQuarterState.
<NoQuarterState.java>

public class NoQuarterState implements State {
    GumballMachine gumballMachine;
 
    public NoQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
 
	public void insertQuarter() {
		System.out.println("You inserted a quarter");
		gumballMachine.setState(gumballMachine.getHasQuarterState());
	}
 
	public void ejectQuarter() {
		System.out.println("You haven't inserted a quarter");
	}
 
	public void turnCrank() {
		System.out.println("You turned, but there's no quarter");
	 }
 
	public void dispense() {
		System.out.println("You need to pay first");
	} 
	
	public void refill() { }
 
	public String toString() {
		return "waiting for quarter";
	}
}

There is no if / elif / else like conditional statements.

Now look at the complete GumballMachine object.

<GumballMachine.java>

public class GumballMachine {
 
	State soldOutState;
	State noQuarterState;
	State hasQuarterState;
	State soldState;
	State winnerState;
 
	State state = soldOutState;
	int count = 0;
 
	public GumballMachine(int numberGumballs) {
		soldOutState = new SoldOutState(this);
		noQuarterState = new NoQuarterState(this);
		hasQuarterState = new HasQuarterState(this);
		soldState = new SoldState(this);
		winnerState = new WinnerState(this);

		this.count = numberGumballs;
 		if (numberGumballs > 0) {
			state = noQuarterState;
		} 
	}
 
	public void insertQuarter() {
		state.insertQuarter();
	}
 
	public void ejectQuarter() {
		state.ejectQuarter();
	}
 
	public void turnCrank() {
		state.turnCrank();
		state.dispense();
	}

	void setState(State state) {
		this.state = state;
	}
 
	void releaseBall() {
		System.out.println("A gumball comes rolling out the slot...");
		if (count > 0) {
			count = count - 1;
		}
	}
 
	int getCount() {
		return count;
	}
 
	void refill(int count) {
		this.count += count;
		System.out.println("The gumball machine was just refilled; its new count is: " + this.count);
		state.refill();
	}

    	public State getState() {
        	return state;
    	}

        public State getSoldOutState() {
            return soldOutState;
        }

        public State getNoQuarterState() {
            return noQuarterState;
        }

        public State getHasQuarterState() {
            return hasQuarterState;
        }

        public State getSoldState() {
            return soldState;
        }

        public State getWinnerState() {
            return winnerState;
        }

        public String toString() {
            StringBuffer result = new StringBuffer();
            result.append("\nMighty Gumball, Inc.");
            result.append("\nJava-enabled Standing Gumball Model #2004");
            result.append("\nInventory: " + count + " gumball");
            if (count != 1) {
                result.append("s");
            }
            result.append("\n");
            result.append("Machine is " + state + "\n");
            return result.toString();
        }
}

Now, for the actions. These are amazingly simple to implement now. We just delegate to the current state.
Note that we don't need an action method for dispense() in GumballMachine because it's just an internal action; a user can't ask the machine to dispense directly. But we do call dispense() on the state object from the turnCrank() method.

What we've done so far?

  • We localised the behaviour of each state into its own class.
  • Removed all the traoublesome if statements that would have been difficult to maintiain.
  • Closed each state for modification, and yet left the Gumball Machine open to extension by adding new state classes (and we'll do this in a second).
  • Created a code base and class structure that maps much more closely to th Might Gumball diagram an is easier to read and understand.

Why is it good? 🧐

  • Single Responsibility Principle.
    Organise the code related to particular states into separate classes.
  • Open CLosed Principle.
    Introduce new states without changing existing state classes or the context.
  • Simplify the code of the context by eliminating bulky state machine conditionals.

Application

Use the State 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. Then the 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 Pattern when you have a lot of duplicate code aross similar states and transitions of a condition-based state machine. Then the pattern lets you compose hierarchies of state classes and reduce duplication by extracting common code into abstract base classes.

profile
Keep Learning👨🏻‍💻

0개의 댓글