Overview
The objective is to design a vending machine. Please note that designing a Vending Machine is an Object-Oriented Problem. It is not a distributed system problem. So we need to approach it that way. Below are the requirements
- A vending machine will allow users to select the item
- A vending machine will allow the admin to add the items
- Once the item is selected the user can insert the money. The item will be dispensed after money is entered
- A vending machine will have different states.
For simplicity, let’s assume that vending machine only has one type of item or product. Also for simplicity let’s assume that a Vending Machine can be in 4 different states
- hasItem
- noItem
- itemRequested
- hasMoney
A vending machine will also have different actions. Again for simplicity lets assume that there are only four actions:
- Select the item
- Add the item
- Insert Money
- Dispense Item
We can use a state design pattern here to design the vending machine. State design pattern is a behavioral design pattern that is based on a Finite State Machine.
Now the question is why we are using a State Design Pattern to design a Vending Machine? Below are two reasons
- State design pattern is used when an object can be in many different states. Depending upon the current request the object needs to change its current state
- Vending machines can be in many different states. A Vending Machine will move from one state to another. Let’s say Vending Machine is in itemRequested then it will move to hasMoney state once the action “Insert Money” is done
- State design pattern is used when an object will have different responses to the same request depending upon the current state. Using state design pattern here will prevent a lot of conditional statements
- For example in the case of Vending Machine, if a user wants to purchase an item then the machine will proceed if it is hasItemState or it will reject if it is in noItemState. If you notice here that the Vending Machine on the request of purchase of an item gives two different responses depending upon whether it is in hasItemState or noItemState. With this approach, our code will not have any kind of conditional statement. All the logic is being handled by concrete state implementations.
UML Diagram
Below is the UML diagram of the Vending Machine
Here is the overall idea
- We will have an interface “State” which defines signatures of functions which represents action in the context of Vending Machine. Below are the actions function signatures
- addItem(int) error
- requestItem() error
- insertMoney(money int) error
- dispenseItem() error
- Each of the concrete states implements all 4 above functions and either move to another state on these actions or gives some response.
- Each of the concrete states also embeds a pointer to the current Vending Machine object so that state transition can happen on that object.
Low–Level Design
Below is the low-level design expressed in the Go programming language. Later we will see a working example as well
Vending Machine Class
type vendingMachine struct {
hasItem state
itemRequested state
hasMoney state
noItem state
currentState state
itemCount int
itemPrice int
}
func (v *vendingMachine) requestItem() error
func (v *vendingMachine) addItem(count int) error
func (v *vendingMachine) insertMoney(money int) error
func (v *vendingMachine) dispenseItem() error
func (v *vendingMachine) setState(s state)
func (v *vendingMachine) incrementItemCount(count int)
State Interface
type state interface {
addItem(int) error
requestItem() error
insertMoney(money int) error
dispenseItem() error
}
NoItem State Class
type noItemState struct {
vendingMachine *vendingMachine
}
func (i *noItemState) requestItem() error
func (i *noItemState) addItem(count int) error
func (i *noItemState) insertMoney(money int) error
func (i *noItemState) dispenseItem() error
Item Requested State Class
type itemRequestedState struct {
vendingMachine *vendingMachine
}
func (i *itemRequestedState) requestItem() error
func (i *itemRequestedState) addItem(count int) error
func (i *itemRequestedState) insertMoney(money int) error
func (i *itemRequestedState) dispenseItem() error
Has Item State Class
type hasItemState struct {
vendingMachine *vendingMachine
}
func (i *hasItemState) requestItem() error
func (i *hasItemState) addItem(count int) error
func (i *hasItemState) insertMoney(money int) error
func (i *hasItemState) dispenseItem() error
Has Money State Class
type hasMoneyState struct {
vendingMachine *vendingMachine
}
func (i *hasMoneyState) requestItem() error
func (i *hasMoneyState) addItem(count int) error
func (i *hasMoneyState) insertMoney(money int) error
func (i *hasMoneyState) dispenseItem() error
Program
Here is the full working code if anyone is interested in the Go programming language
vendingMachine.go
package main
import "fmt"
type vendingMachine struct {
hasItem state
itemRequested state
hasMoney state
noItem state
currentState state
itemCount int
itemPrice int
}
func newVendingMachine(itemCount, itemPrice int) *vendingMachine {
v := &vendingMachine{
itemCount: itemCount,
itemPrice: itemPrice,
}
hasItemState := &hasItemState{
vendingMachine: v,
}
itemRequestedState := &itemRequestedState{
vendingMachine: v,
}
hasMoneyState := &hasMoneyState{
vendingMachine: v,
}
noItemState := &noItemState{
vendingMachine: v,
}
v.setState(hasItemState)
v.hasItem = hasItemState
v.itemRequested = itemRequestedState
v.hasMoney = hasMoneyState
v.noItem = noItemState
return v
}
func (v *vendingMachine) requestItem() error {
return v.currentState.requestItem()
}
func (v *vendingMachine) addItem(count int) error {
return v.currentState.addItem(count)
}
func (v *vendingMachine) insertMoney(money int) error {
return v.currentState.insertMoney(money)
}
func (v *vendingMachine) dispenseItem() error {
return v.currentState.dispenseItem()
}
func (v *vendingMachine) setState(s state) {
v.currentState = s
}
func (v *vendingMachine) incrementItemCount(count int) {
fmt.Printf("Adding %d items\n", count)
v.itemCount = v.itemCount + count
}
state.go
package main
type state interface {
addItem(int) error
requestItem() error
insertMoney(money int) error
dispenseItem() error
}
noItemState.go
package main
import "fmt"
type noItemState struct {
vendingMachine *vendingMachine
}
func (i *noItemState) requestItem() error {
return fmt.Errorf("Item out of stock")
}
func (i *noItemState) addItem(count int) error {
i.vendingMachine.incrementItemCount(count)
i.vendingMachine.setState(i.vendingMachine.hasItem)
return nil
}
func (i *noItemState) insertMoney(money int) error {
return fmt.Errorf("Item out of stock")
}
func (i *noItemState) dispenseItem() error {
return fmt.Errorf("Item out of stock")
}
itemRequestedState.go
package main
import "fmt"
type itemRequestedState struct {
vendingMachine *vendingMachine
}
func (i *itemRequestedState) requestItem() error {
return fmt.Errorf("Item already requested")
}
func (i *itemRequestedState) addItem(count int) error {
return fmt.Errorf("Item Dispense in progress")
}
func (i *itemRequestedState) insertMoney(money int) error {
if money < i.vendingMachine.itemPrice {
fmt.Errorf("Inserted money is less. Please insert %d", i.vendingMachine.itemPrice)
}
fmt.Println("Money entered is ok")
i.vendingMachine.setState(i.vendingMachine.hasMoney)
return nil
}
func (i *itemRequestedState) dispenseItem() error {
return fmt.Errorf("Please insert money first")
}
hasItemState.go
package main
import "fmt"
type hasItemState struct {
vendingMachine *vendingMachine
}
func (i *hasItemState) requestItem() error {
if i.vendingMachine.itemCount == 0 {
i.vendingMachine.setState(i.vendingMachine.noItem)
return fmt.Errorf("No item present")
}
fmt.Printf("Item requestd\n")
i.vendingMachine.setState(i.vendingMachine.itemRequested)
return nil
}
func (i *hasItemState) addItem(count int) error {
fmt.Printf("%d items added\n", count)
i.vendingMachine.incrementItemCount(count)
return nil
}
func (i *hasItemState) insertMoney(money int) error {
return fmt.Errorf("Please select item first")
}
func (i *hasItemState) dispenseItem() error {
return fmt.Errorf("Please select item first")
}
hasMoneyState.go
package main
import "fmt"
type hasMoneyState struct {
vendingMachine *vendingMachine
}
func (i *hasMoneyState) requestItem() error {
return fmt.Errorf("Item dispense in progress")
}
func (i *hasMoneyState) addItem(count int) error {
return fmt.Errorf("Item dispense in progress")
}
func (i *hasMoneyState) insertMoney(money int) error {
return fmt.Errorf("Item out of stock")
}
func (i *hasMoneyState) dispenseItem() error {
fmt.Println("Dispensing Item")
i.vendingMachine.itemCount = i.vendingMachine.itemCount - 1
if i.vendingMachine.itemCount == 0 {
i.vendingMachine.setState(i.vendingMachine.noItem)
} else {
i.vendingMachine.setState(i.vendingMachine.hasItem)
}
return nil
}
main.go
package main
import (
"fmt"
"log"
)
func main() {
vendingMachine := newVendingMachine(1, 10)
err := vendingMachine.requestItem()
if err != nil {
log.Fatalf(err.Error())
}
err = vendingMachine.insertMoney(10)
if err != nil {
log.Fatalf(err.Error())
}
err = vendingMachine.dispenseItem()
if err != nil {
log.Fatalf(err.Error())
}
fmt.Println()
err = vendingMachine.addItem(2)
if err != nil {
log.Fatalf(err.Error())
}
fmt.Println()
err = vendingMachine.requestItem()
if err != nil {
log.Fatalf(err.Error())
}
err = vendingMachine.insertMoney(10)
if err != nil {
log.Fatalf(err.Error())
}
err = vendingMachine.dispenseItem()
if err != nil {
log.Fatalf(err.Error())
}
}
Output:
Item requestd
Money entered is ok
Dispensing Item
Adding 2 items
Item requestd
Money entered is ok
Dispensing Item