Skip to content
Go back

Design Patterns in Practice: Singleton and Observer with a Lucky Dip Machine

6 min read

This blog post demonstrates a practical use case where the Singleton and Observer design patterns become important in real-world applications.

The Problem: Arcade Lucky Dip Machine

Imagine you’re in an arcade with a Lucky Dip Machine. This machine has a limited inventory of prizes, and multiple people can use it throughout the day. Here are the key requirements:

Design Overview

┌────────────────────────────────────────────────────────┐
│ LUCKY DIP MACHINE SYSTEM │
├────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────────────────────┐ │
│ │ User │──> │ LuckyDipMachine │ │
│ │ (Client) │ │ (Singleton) │ │
│ └─────────────┘ │ • Private constructor │ │
│ │ • Static instance │ │
│ │ • Inventory management │ │
│ └─────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Observers │ │
│ │ • Track │ │
│ │ • Notify │ │
│ └─────────────┘ │
│ │
└────────────────────────────────────────────────────────┘

Implementation

1. The Prize Class

First, let’s create a simple Prize class to represent the items in our machine:

package me.jianliew;
public class Prize {
private String name;
public Prize(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Prize prize = (Prize) o;
return getName().equals(prize.getName());
}
@Override
public int hashCode() {
return getName().hashCode();
}
@Override
public String toString() {
return "Prize{name='" + name + "'}";
}
}

2. The LuckyDipMachine (Singleton + Observable)

This is where the magic happens. Our LuckyDipMachine implements both patterns:

package me.jianliew;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.*;
public class LuckyDipMachine {
// Singleton: Static instance
private static LuckyDipMachine ourInstance = new LuckyDipMachine();
// Observer: PropertyChangeSupport for notifications
private PropertyChangeSupport support;
// Inventory management
private Map<Prize, Integer> inventory;
// Singleton: Private constructor
private LuckyDipMachine() {
inventory = new HashMap<>();
support = new PropertyChangeSupport(this);
fill();
}
// Singleton: Public access method
public static LuckyDipMachine getInstance() {
return ourInstance;
}
private void fill() {
Prize p1 = new Prize("Potato");
Prize p2 = new Prize("Tomato");
inventory.put(p1, 2);
inventory.put(p2, 2);
}
public Prize getRandomPrize() {
List<Prize> keysAsArray = new ArrayList<>(inventory.keySet());
Random r = new Random();
return keysAsArray.get(r.nextInt(keysAsArray.size()));
}
public void pull() {
if (inventory.isEmpty()) return;
Prize p = getRandomPrize();
inventory.computeIfPresent(p, (prize, integer) -> inventory.get(prize) - 1);
if (inventory.get(p) == 0) {
inventory.remove(p);
}
// Observer: Notify all listeners
support.firePropertyChange("prize", "", p);
}
public int getInventorySize() {
return inventory.values().stream().mapToInt(Integer::intValue).sum();
}
// Observer: Add/remove listeners
public void addPropertyChangeListener(PropertyChangeListener pcl) {
support.addPropertyChangeListener(pcl);
}
public void removePropertyChangeListener(PropertyChangeListener pcl) {
support.removePropertyChangeListener(pcl);
}
@Override
public String toString() {
return "LuckyDipMachine{inventory=" + inventory + "}";
}
}

3. The Observer

Our Observer implements PropertyChangeListener to receive notifications:

package me.jianliew;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
public class Observer implements PropertyChangeListener {
private List<Prize> observedPrizes;
public Observer() {
observedPrizes = new ArrayList<>();
}
@Override
public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
Prize p = (Prize) propertyChangeEvent.getNewValue();
this.observedPrizes.add(p);
System.out.println(toString());
}
@Override
public String toString() {
return "Observer{observedPrizes=" + observedPrizes + "}";
}
}

4. Testing the System

Here’s how to test our implementation:

package me.jianliew;
public class Test {
public static void main(String[] args) {
Observer o = new Observer();
LuckyDipMachine ldm = LuckyDipMachine.getInstance();
ldm.addPropertyChangeListener(o);
System.out.println("Initial inventory: " + ldm.getInventorySize());
ldm.pull();
ldm.pull();
ldm.pull();
// Prove it's a singleton
LuckyDipMachine ldm2 = LuckyDipMachine.getInstance();
System.out.println("After 3 pulls: " + ldm2.getInventorySize());
ldm2.pull();
System.out.println("Final inventory: " + ldm.getInventorySize());
}
}

Expected Output

Initial inventory: 4
Observer{observedPrizes=[Prize{name='Tomato'}]}
Observer{observedPrizes=[Prize{name='Tomato'}, Prize{name='Potato'}]}
Observer{observedPrizes=[Prize{name='Tomato'}, Prize{name='Potato'}, Prize{name='Tomato'}]}
After 3 pulls: 1
Observer{observedPrizes=[Prize{name='Tomato'}, Prize{name='Potato'}, Prize{name='Tomato'}, Prize{name='Potato'}]}
Final inventory: 0

Key Design Pattern Benefits

Singleton Pattern

Observer Pattern

Lessons Learned

  1. Always override equals() and hashCode() when using objects in collections like HashMap
  2. PropertyChangeSupport is much easier to use than the Observable interface
  3. computeIfPresent() provides elegant lambda-based map updates
  4. Singleton pattern has multiple implementation approaches - this is the simplest but not thread-safe
  5. Design is subjective - you could debate whether quantity belongs in the Prize class itself

When to Use These Patterns

Conclusion

This Lucky Dip Machine example demonstrates how design patterns work together to create robust, maintainable systems. The Singleton pattern ensures there’s only one machine, while the Observer pattern lets everyone know when someone wins a prize. Together, they create a system that’s both controlled and informative.

The key takeaway is that design patterns aren’t just theoretical concepts—they solve real problems in elegant ways. By understanding when and how to apply them, you can write code that’s more maintainable, extensible, and easier to understand.


Design patterns provide proven solutions to common software design problems. This example shows how combining multiple patterns can create systems that are both powerful and easy to work with.


Share this post on:

Previous Post
TSP Algorithm: Solving the Traveling Salesman Problem with Genetic Algorithms
Next Post
k-Nearest Neighbour on Maps