Programming lesson
Mastering Object-Oriented Design in Java: Building a Store Management System Like McGill's Trottier Convenience Store
Learn Java OOP principles by modeling a convenience store inventory system. This tutorial covers abstract classes, inheritance, encapsulation, and polymorphism using a real-world assignment example from ECSE250.
Why Object-Oriented Programming Matters for Modern Apps
Object-oriented programming (OOP) is the backbone of many modern software systems, from the AI assistants powering e-commerce platforms to the backend of viral apps like BeReal or ChatGPT. In this tutorial, you'll learn how to apply OOP concepts by building a store management system similar to what you'd find in McGill's Trottier convenience store. We'll focus on Java inheritance, abstract classes, and encapsulation—key topics for any Java beginner or student tackling assignments like ECSE250 Assignment 1.
Understanding the Problem: Modeling a Store's Inventory
Imagine you're tasked with programming an AI that monitors purchases at a university convenience store. You need to represent various products—drinks, snacks, and special items—while reusing code efficiently. This is where OOP design patterns shine. Instead of writing separate classes for each product type with duplicated fields (price, happiness index), you create a hierarchy using abstract classes and inheritance.
Key Classes in the System
- StoreItem (abstract): Base class with price and happiness index.
- Drink (abstract): Extends StoreItem, adds bottle count and buzziness.
- Snack, FizzWiz, SnoozeJuice: Concrete subclasses.
- ItemList: Custom data structure (no ArrayList allowed!).
- Store: Manages inventory and purchases.
Step 1: Building the Abstract Class StoreItem
Start with the foundation. StoreItem is an abstract class because you'll never instantiate a generic item—only specific products. It holds price (double) and happinessIndex (int), both private to enforce encapsulation.
package assignment1.items;
public abstract class StoreItem {
private double price;
private int happinessIndex;
public StoreItem(double price, int happinessIndex) {
if (price < 0 || happinessIndex < 0) {
throw new IllegalArgumentException("Price and happiness must be non-negative.");
}
this.price = price;
this.happinessIndex = happinessIndex;
}
public final double getPrice() { return price; }
public int getHappinessIndex() { return happinessIndex; }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
StoreItem other = (StoreItem) obj;
return Math.abs(this.price - other.price) < 0.001 &&
this.happinessIndex == other.happinessIndex;
}
}Notice the final keyword on getPrice()—subclasses cannot override it. The equals() method uses a tolerance for double comparison, a common pitfall in Java programming. This design ensures code reusability and data hiding.
Step 2: Extending with Drink Class
Now let's model drinks. Drink extends StoreItem and adds fields: numOfBottles (protected int) and isBuzzy (private boolean). Static fields like MAX_PACK_SIZE (6) and BUZZY_HAPPINESS_BOOST (1) are shared across all drinks.
package assignment1.items;
public abstract class Drink extends StoreItem {
public static int MAX_PACK_SIZE = 6;
public static int BUZZY_HAPPINESS_BOOST = 1;
protected int numOfBottles;
private boolean isBuzzy;
public Drink(double price, int happinessIndex, int numOfBottles, boolean isBuzzy) {
super(price, happinessIndex);
this.numOfBottles = numOfBottles;
this.isBuzzy = isBuzzy;
}
public int getNumOfBottles() { return numOfBottles; }
@Override
public int getHappinessIndex() {
if (isBuzzy) {
return super.getHappinessIndex() + BUZZY_HAPPINESS_BOOST;
}
return super.getHappinessIndex();
}
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) return false;
if (!(obj instanceof Drink)) return false;
Drink other = (Drink) obj;
return this.isBuzzy == other.isBuzzy;
}
public boolean combine(Drink other) {
// Implementation: check if same type and not same object, then combine bottles
if (other == null || this == other) return false;
if (!this.equals(other)) return false;
if (this.numOfBottles + other.numOfBottles > MAX_PACK_SIZE) return false;
this.numOfBottles += other.numOfBottles;
return true;
}
}Notice how getHappinessIndex() overrides the parent method to add a boost for buzzy drinks. This is polymorphism in action. The combine() method lets you merge two identical drink packs, a feature reminiscent of inventory management in games like Fortnite or Minecraft.
Step 3: Concrete Subclasses and the Snack Hierarchy
Now create concrete classes like FizzWiz (a buzzy drink), SnoozeJuice (non-buzzy), and Snack. Each must implement any remaining abstract methods (though StoreItem has none). Here's an example for Snack:
package assignment1.items;
public class Snack extends StoreItem {
public Snack(double price, int happinessIndex) {
super(price, happinessIndex);
}
}That's it! Because Snack doesn't add extra fields, it inherits everything from StoreItem. This demonstrates hierarchical inheritance and keeps code clean.
Step 4: Building a Custom Data Structure - ItemList
The assignment forbids using ArrayList or LinkedList. So you'll implement a simple dynamic array—a great exercise in data structures and array manipulation. Your ItemList class should support adding, removing, and retrieving items.
package assignment1.items;
public class ItemList {
private StoreItem[] items;
private int size;
public ItemList() {
items = new StoreItem[10];
size = 0;
}
public void add(StoreItem item) {
if (size == items.length) {
// resize
StoreItem[] newArray = new StoreItem[items.length * 2];
System.arraycopy(items, 0, newArray, 0, items.length);
items = newArray;
}
items[size++] = item;
}
public StoreItem get(int index) {
if (index < 0 || index >= size) throw new IndexOutOfBoundsException();
return items[index];
}
public boolean remove(StoreItem item) {
for (int i = 0; i < size; i++) {
if (items[i].equals(item)) {
// shift elements left
System.arraycopy(items, i+1, items, i, size - i - 1);
items[--size] = null;
return true;
}
}
return false;
}
public int size() { return size; }
}This custom list uses dynamic resizing similar to how ArrayList works internally. It's a perfect example of implementing data structures from scratch.
Step 5: The Store Class - Bringing It All Together
The Store class uses ItemList to manage inventory and handle purchases. It should have methods like addItem(), removeItem(), searchByName(), and purchase(). Here's a skeleton:
package assignment1;
import assignment1.items.*;
public class Store {
private ItemList inventory;
private double balance;
public Store() {
inventory = new ItemList();
balance = 0.0;
}
public void addItem(StoreItem item) {
inventory.add(item);
}
public boolean purchase(StoreItem item) {
// Find item in inventory, remove it, and add price to balance
for (int i = 0; i < inventory.size(); i++) {
StoreItem invItem = inventory.get(i);
if (invItem.equals(item)) {
inventory.remove(invItem);
balance += invItem.getPrice();
return true;
}
}
return false;
}
public double getBalance() { return balance; }
}Applying OOP Principles: Why This Design Works
This assignment teaches several OOP principles:
- Abstraction: Abstract classes define common behavior.
- Inheritance: Subclasses reuse and extend parent code.
- Polymorphism: Methods behave differently based on actual object type (e.g.,
getHappinessIndex()). - Encapsulation: Private fields with public getters protect data.
Think of this like building a leaderboard for a gaming tournament: each player (object) has base stats (health, score) and special abilities (like buzzy boost). The system calculates final scores polymorphically.
Common Pitfalls and Tips
- Double comparison: Always use a tolerance, not
==. - Equals symmetry: Ensure
equals()works both ways. - Package structure: Use
package assignment1.items;for item classes. - No imports: Don't use
java.util.ArrayList—it's forbidden. - UML diagrams: Draw them to visualize class relationships before coding.
Relating to Real-World Trends
Just as AI assistants like Siri or Google Assistant manage tasks, your store AI manages inventory. The combine() method mirrors how you might merge duplicate items in a game like Stardew Valley or Pokémon GO. And the custom ItemList is a reminder that understanding data structures is crucial for performance—even in high-frequency trading systems or fintech apps.
Conclusion
By completing this tutorial, you've practiced Java OOP, built a custom data structure, and designed a class hierarchy that models a real-world system. These skills are directly applicable to software engineering roles, game development, and backend development. Keep practicing with assignments like ECSE250 Assignment 1 to solidify your understanding. Happy coding!