Skip to content

Instantly share code, notes, and snippets.

@bign8
Created October 4, 2016 00:10
Show Gist options
  • Save bign8/cd520dd13c72c233a0d22b278605df2c to your computer and use it in GitHub Desktop.
Save bign8/cd520dd13c72c233a0d22b278605df2c to your computer and use it in GitHub Desktop.
ESOF-322 State Pattern Notes
package main
import (
"bufio"
"flag"
"fmt"
"math/rand"
"os"
)
var (
count = flag.Int("count", 5, "inital volume of gumball machine")
_ State = (*stateSoldOut)(nil)
_ State = (*stateNoQuarter)(nil)
_ State = (*stateHasQuarter)(nil)
_ State = (*stateSold)(nil)
_ State = (*stateWinner)(nil)
)
// State exposes the methods that can be performed on a gumball Machine
type State interface {
String() string
Insert()
Eject()
Crank()
dispense()
}
// GumballMachine contains the core switching logic for a gumball machine
type GumballMachine struct {
soldOut State
noQuarter State
hasQuarter State
sold State
winner State
state State
count int
}
// NewGumballMachine consntructs a new gumball machine
func NewGumballMachine(count int) *GumballMachine {
gm := &GumballMachine{count: count}
gm.soldOut = &stateSoldOut{gm}
gm.noQuarter = &stateNoQuarter{gm}
gm.hasQuarter = &stateHasQuarter{gm}
gm.sold = &stateSold{gm}
gm.winner = &stateWinner{gm}
if count > 0 {
gm.state = gm.noQuarter
} else {
gm.state = gm.soldOut
}
return gm
}
func (gm *GumballMachine) Insert() { gm.state.Insert() }
func (gm *GumballMachine) Eject() { gm.state.Eject() }
func (gm *GumballMachine) Crank() {
gm.state.Crank()
gm.state.dispense()
}
func (gm *GumballMachine) Refil(count int) {
gm.count += count
if gm.state == gm.soldOut {
gm.state = gm.noQuarter
}
}
func (gm *GumballMachine) String() string {
return fmt.Sprintf("%s (%d)", gm.state.String(), gm.count)
}
func (gm *GumballMachine) release() {
fmt.Println("A gumball comes rolling out the slot...")
if gm.count != 0 {
gm.count--
}
}
type stateSoldOut struct{ gm *GumballMachine }
func (s *stateSoldOut) Insert() { fmt.Println("Can't insert") }
func (s *stateSoldOut) Eject() { fmt.Println("Can't eject") }
func (s *stateSoldOut) Crank() { fmt.Println("Can't crank") }
func (s *stateSoldOut) dispense() {}
func (s *stateSoldOut) String() string { return "Sold Out" }
type stateNoQuarter struct{ gm *GumballMachine }
func (s *stateNoQuarter) Insert() { s.gm.state = s.gm.hasQuarter }
func (s *stateNoQuarter) Eject() { fmt.Println("Can't eject") }
func (s *stateNoQuarter) Crank() { fmt.Println("Can't crank") }
func (s *stateNoQuarter) dispense() {}
func (s *stateNoQuarter) String() string { return "No Quarter" }
type stateHasQuarter struct{ gm *GumballMachine }
func (s *stateHasQuarter) Insert() { fmt.Println("You can't insert another quarter") }
func (s *stateHasQuarter) Eject() { s.gm.state = s.gm.noQuarter }
func (s *stateHasQuarter) Crank() {
if rand.Intn(10) == 0 && s.gm.count > 1 {
fmt.Println("Your A Winner!!!")
s.gm.state = s.gm.winner
} else {
s.gm.state = s.gm.sold
}
}
func (s *stateHasQuarter) dispense() {}
func (s *stateHasQuarter) String() string { return "Has Quarter" }
type stateSold struct{ gm *GumballMachine }
func (s *stateSold) Insert() { fmt.Println("Can't insert") }
func (s *stateSold) Eject() { fmt.Println("Can't eject") }
func (s *stateSold) Crank() { fmt.Println("Can't crank") }
func (s *stateSold) dispense() {
s.gm.release()
if s.gm.count > 0 {
s.gm.state = s.gm.noQuarter
} else {
s.gm.state = s.gm.soldOut
}
} // TODO
func (s *stateSold) String() string { return "Sold" }
type stateWinner struct{ gm *GumballMachine }
func (s *stateWinner) Insert() { fmt.Println("Can't insert") }
func (s *stateWinner) Eject() { fmt.Println("Can't eject") }
func (s *stateWinner) Crank() { fmt.Println("Can't crank") }
func (s *stateWinner) dispense() {
for i := 0; i < 2; i++ {
s.gm.release()
if s.gm.count == 0 {
s.gm.state = s.gm.soldOut
return
}
}
s.gm.state = s.gm.noQuarter
} // TODO
func (s *stateWinner) String() string { return "Winner" }
func main() {
flag.Parse()
gm := NewGumballMachine(*count)
scan := bufio.NewReader(os.Stdin)
for {
fmt.Print(gm.String() + "; Insert, Eject, Crank, Refil > ")
switch text, _ := scan.ReadString('\n'); text[0] {
case 'I':
fallthrough
case 'i':
gm.Insert()
case 'E':
fallthrough
case 'e':
gm.Eject()
case 'C':
fallthrough
case 'c':
gm.Crank()
case 'R':
fallthrough
case 'r':
gm.Refil(*count)
case '\n':
default:
fmt.Printf("Unknown Command: %q\n", text)
}
}
}
/*
Guest Lecture: Nate Woods
Class: ESOF-322
Date: Tuesday Oct 4 (12:15pm to 1:30pm)
Location: Cheever Hall 215
Topic: The State Pattern
*/
// # Gumballs!
// Imagine a gumball machine
// What can you do with it?
// # State Diagrams
// TODO: (insert state diagram here)
// ## States
// - Has Quarter
// - No Quarter
// - Gumball Sold
// - Out Of Gumballs
// ## Actions
// - Insert Quarter
// - Eject Quarter
// - Turns Crank
// - Dispense (Gumballs Condition)
// States are just different configurations of the machine
// that behave in a certain way and need some action to take them to another state.
// -------------------------- START DEMO --------------------------
public class GumballMachine {
// States represented as unique integers
final static int SOLD_OUT = 0; // Out Of Gumballs
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3;
// Current State of the GumballMachine
int state = SOLD_OUT;
int count = 0;
public GumballMachine(int count) {
this.count = count;
if (count > 0) state = NO_QUARTER;
}
// For each action we create a method
public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("You can't insert another 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");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("You inserted a querter");
}
}
public void ejectQuarter() {
if (state == HAS_QUARTER) {
System.out.println("Quarter returned");
state = NO_QUARTER;
} else if (state == NO_QUARTER) {
System.out.println("You haven't inserted a quarter");
} else if (state == SOLD_OUT) {
System.out.println("You can't eject, you haven't inserted a quarter yet");
} else if (state == SOLD) {
System.out.println("Sorry, you already turned the crank");
}
}
public void turnCrank() {
if (state == SOLD) {
System.out.println("Turning twice doesn't get you another gumball!")
} else if (state == NO_QUERTER) {
System.out.println("You turned but there's no quarter");
} else if (state == SOLD_OUT) {
System.out.println("You turned , but there are no gumballs");
} else if (state == HAS_QUARTER) {
System.out.println("You turned...");
state = SOLD;
dispense();
}
}
public void dispense() {
if (state == SOLD) {
System.out.println("A gumball comes rolling out the slot");
count = count - 1;
if (count == 0) {
System.out.println("Oops, out of gumballs!");
state = SOLD_OUT;
} else {
state = NO_QUARTER;
}
} else if (state == NO_QUARTER) {
System.out.println("You need to pay first");
} else if (state == SOLD_OUT) {
System.out.println("No gumball dispensed");
} else if (State == HAS_QUARTER) {
System.out.println("No gumball dispensed");
}
}
// other methods like toString() and refill()
}
public class GumballMachineTestDriver {
public static void main(String[] args) {
GumballMachine gm = new GumballMachine(5);
System.out.println(gm);
gm.insertQuarter();
gm.turnCrank();
System.out.println(gm);
gm.insertQuarter();
gm.ejectQuarter();
gm.turnCrank(); // no gumball for you
System.out.println(gm);
gm.insertQuarter();
gm.turnCrank(); // should get a gumball
gm.insertQuarter();
gm.turnCrank(); // should get a gumball
gm.ejectQuarter(); // shouldn't get a quarter back
System.out.println(gm);
gm.insertQuarter();
gm.insertQuarter(); // nope
gm.turnCrank();
gm.insertQuarter(); // stress testing
gm.turnCrank();
gm.insertQuarter();
gm.turnCrank();
System.out.println(gm);
}
}
// -------------------------- END DEMO --------------------------
// # Change: One in ten get a free gumball!!!
// 10% of the time when the crank is turned
// the customer gets two gumballs!
// TODO: insert new state diagram here
// See just this addition reveals the awkward design we have
public class GumballMachineChanges {
final static int SOLD_OUT = 0;
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3;
// add a new winner state
public void insertQuarter() { /* lots of if statements */ }
public void ejectQuarter() { /* lots of if statements */ }
public void turnCrank() { /* lots of if statements */ } // Add Winner logic
public void dispense() { /* lots of if statements */ }
}
// Pain points
// - Open Closed Principle: open for extension / closed for modification
// - This code would make a FORTRAN programmer proud
// - Not object oriented
// - Transitions are burried
// - Haven't encapsulated anything that varies here
// - Further additions are likely to cause bugs in working code
// -------------------------- START DEMO --------------------------
public interface State {
public void insertQuarter();
public void ejectQuarter();
public void turnCrank();
public void dispense();
}
public class NoQuarterState implements State {
GumballMachine gm;
public NoQuarterState(GumballMachine gm) {
this.gm = gm;
}
public void insertQuarter() {
System.out.println("You inserted a quarter");
gm.setState(gm.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 class GumballMachine {
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
State state = soldOutState;
int count = 0;
public NewGumballMachine(int count) {
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
this.count = count;
if (count > 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;
}
// More methods including getters for each state (getCount(), getNoQuarterState())
}
public class HasQuarterState implements State {
GumballMachine gm;
public HasQuarterState(GumballMachine gm) {
this.gm = gm;
}
public void insertQuarter() { // Invalid action for this state
System.out.println("You can't insert another quarter");
}
public void ejectQuarter() {
System.out.println("Quarter returned");
gm.setState(gm.getNoQuarterState());
}
public void turnCrank() {
System.out.println("You turned...");
gm.setState(gm.getSoldState());
}
public void dispense() { // Invalid action for this state
System.out.println("No gumball dispensed");
}
}
public class SoldState implements State {
GumballMachine gm;
public HasQuarterState(GumballMachine gm) {
this.gm = gm;
}
public void insertQuarter() { // Invalid action for this state
System.out.println("Please wait, we'er already giving you a gumball");
}
public void ejectQuarter() { // Invalid action for this state
System.out.println("Sorry, you already turned the crank");
}
public void turnCrank() { // Invalid action for this state
System.out.println("Turning twice doesn't get you another gumball");
}
public void dispense() {
gm.releaseBall();
if (gm.getCount() > 0) {
gm.setState(gm.getNoQuarterState());
} else {
System.out.println("Oops, out of gumballs!");
gm.setState(gm.getSoldOutState());
}
}
}
// -------------------------- END DEMO --------------------------
// - Localized the behavior of each state into it's own class
// - Removed all teh troublesome if statements that would have been difficult to maintain
// - Closed each state for modification, and yet left Gumball Machine open to extension by adding new state clases
// - Created a code base and class structure that maps closely to the Mighty Gumball diagram and is easer to read + understand
// # The State pattern
// Allows an object to alter it's behavior when its internal state changes.
// The object will appear to change it's class.
// TODO: insert UML diagram here!!!
// Note: same as strategy pattern
// Think of the State Pattern as an alternative to putting lost of conditionals in your context;
// By encapsulating the behaviors within state objects, you can simply change the state object in context to change it's behavior
// -------------------------- START DEMO --------------------------
// # Implemnting the game!!!
public class GameGumballMachine {
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
State winnerState; // Brand new!!!
// Added getter/initialization for winnerState
}
public class WinnerState implements State {
// instance variables and constructor
// insertQuarter + ejectQuarter + turnCrank error
public void dispense() {
System.out.println("YOU'RE A WINNER! You get two gumballs for your quarter");
gm.releaseBall();
if (gm.getCount() == 0) gm.setState(gm.getSoldOutState());
else {
gm.releaseBall();
if (gm.getCount() > 0) gm.setState(gm.getNoQuarterState());
else {
System.out.Println("Oops, out of gumballs!");
gm.setState(gm.getSOldOutState());
}
}
}
}
public class HasQuarterState implements State {
Random randomWinner = new Random(System.currentTimeMillis());
GumballMachine gm;
// constructor
// insertQuarter + dispense error
public void ejectQuarter() {
System.out.println("Quarter returned");
gm.setState(gm.getNoQuarterState());
}
public void turnCrank() {
System.out.println("You turned...");
int winner = randomWinner.nextInt(10);
if ((winner == 0) && (gm.getCount() > 1)) {
gm.setState(gm.getWinnerState());
} else {
gm.setState(gm.getSoldState());
}
}
}
// -------------------------- END DEMO --------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment