Demo: modeling the parking garage from requirements
On This Lesson 11 topics
Lesson Overview
This is a hands-on lesson that applies the whole module to a new domain: a parking garage. You will go from a paragraph of text to a small working Java design in a few steps, and you will do one thing on purpose that the previous lessons warned against: put the fee calculation in the wrong place, watch the class grow toward a god class, and then fix it. At the end you write a short rationale and walk through a scenario.
This lesson prepares the module's lab, "Parking garage", where you model and implement your own version with levels, spot sizes, vehicles, tickets, and fees. The code here is a compact stand-in. In the lab, make your own decisions, write your own UML and rationale, and use the design here only as a comparison.
Everything was compiled and run with JDK 21. Terminal commands are for a plain terminal; if you use an IDE, its menus for creating classes and running programs differ by product and version, so check its help. We keep all classes in one file for the demo; in the lab, use separate files and the package practices from BEG-027.
This lesson builds on lessons 1 to 4 of this module, enums and interfaces (BEG-024), and UML (BEG-025).
Learning Objectives
By the end of this lesson you will be able to:
- Extract candidate classes, rules, and behaviors from a requirements text.
- Replace a tempting inheritance hierarchy with a single class and an enum.
- Assign each behavior to the class that has the information.
- Recognize a god class forming and move the logic into a focused class behind an interface.
- Walk through a normal and a failure scenario with real output.
- Write a short design rationale with a rejected alternative.
Written Explanation
Step 1: the requirements
A parking garage has several levels. Each level has parking spots, and each spot is small, medium, or large. A vehicle is also small, medium, or large and needs a spot at least as big as itself. A driver who enters gets a ticket. On leaving, the driver pays a fee based on the hours parked, rounded up to full hours, at a rate that depends on the spot size: 2, 3, or 4 per hour. A ticket can be used only once.
Step 2: nouns, verbs, rules
- Nouns: garage, level, spot, size, vehicle, driver, ticket, fee, hour, rate.
- Verbs: park (enter, get a ticket), leave (pay), fit.
- Rules: a vehicle needs a spot at least as big; a spot holds one vehicle at a time; a ticket is used once; hours are rounded up.
Sorting as in lesson 1:
| Noun | Verdict |
|---|---|
Garage, Level, Spot, Vehicle, Ticket | Classes, each with rules or behavior |
| size | An enum SpotSize with three values; a fixed set of constants |
| driver | Not a class: nothing in the text needs it; the plate on the vehicle is enough |
| fee, rate | A fee rule: a FeePolicy (decided in step 5) |
| hour | A number computed from minutes |
A tempting trap. "Vehicles are small, medium, or large" invites Car, Truck, Motorbike subclasses. But what differs between vehicles in this text is only one value, the size. That is exactly the premature inheritance from lesson 1: subclasses that differ by one piece of data. One Vehicle class with a SpotSize field does the job. Oracle's tutorial describes an enum as the type to use for a fixed set of constants, and notes that an enum can have fields and methods. We use that: the size comparison lives in the enum.
Step 3: assign responsibilities
| Job | Information needed | Owner |
|---|---|---|
| Does this spot fit this vehicle? | Spot's size, whether it is taken, vehicle's size | Spot.canFit(vehicle) (it knows its size and state) |
| Is spot size A big enough for size B? | The order of sizes | SpotSize.canHold(...) |
| Find a spot on a level | The level's spots | Level.findSpotFor(vehicle) |
| How long has this ticket been parked? | Entry time | Ticket.minutesParked(now) |
| A ticket is used once | Ticket's own flag | Ticket.markUsed() |
| Park a vehicle / process leaving | Coordinates several objects | Garage |
| Compute the fee | Minutes and spot size | FeePolicy (see step 5) |
Step 4: sketch the UML
Garage 1 ◆------- * Level
Level 1 ◆------- * Spot
Garage 1 -------- 1 FeePolicy (HourlyFeePolicy implements FeePolicy)
Ticket * -------- 1 Spot
Ticket * -------- 1 Vehicle
Spot / Vehicle -> SpotSize (an enum)Step 5: the planned mistake, then the fix
The first version of Garage.leave calculates the fee itself, because that is where leaving is processed. It looks harmless:
int leave(Ticket t, int now) {
t.markUsed();
t.spot().free();
int hours = (t.minutesParked(now) + 59) / 60;
int rate;
if (t.spot().size() == SpotSize.SMALL) rate = 2;
else if (t.spot().size() == SpotSize.MEDIUM) rate = 3;
else rate = 4;
return hours * rate;
}It compiles and gives the right answer. But apply the "and" test to Garage: it finds spots and manages tickets and knows pricing rules. Think of the next feature requests: a weekend rate, a daily maximum, a free first half hour. Every one edits Garage, in the middle of the code that frees spots. Each request adds another if, and the class slowly turns into a god class with pricing mixed into parking. The rate also does not belong to the garage's main job; it changes for business reasons.
The fix is the extraction from lesson 3: a FeePolicy interface with one implementation, and Garage only calls it.
interface FeePolicy { int fee(int minutes, SpotSize size); }
class HourlyFeePolicy implements FeePolicy {
public int fee(int minutes, SpotSize size) {
int hours = (minutes + 59) / 60; // round up to whole hours
int rate = 2 + size.ordinal(); // 2, 3, 4 per hour
return hours * rate;
}
}size.ordinal() is the position of the constant in the enum declaration (0, 1, 2 here). It is a shortcut that works only while SMALL, MEDIUM, LARGE stay in this order and the rates stay a straight line. We will list it as a risk in the rationale. After the move, the output was identical to the version with the if chain.
Step 6: the whole design, with a scenario
import java.util.ArrayList;
import java.util.List;
enum SpotSize {
SMALL, MEDIUM, LARGE;
boolean canHold(SpotSize vehicleSize) { return this.ordinal() >= vehicleSize.ordinal(); }
}
class Vehicle {
private final String plate;
private final SpotSize size;
Vehicle(String plate, SpotSize size) { this.plate = plate; this.size = size; }
String plate() { return plate; }
SpotSize size() { return size; }
}
class Spot {
private final String id;
private final SpotSize size;
private boolean occupied;
Spot(String id, SpotSize size) { this.id = id; this.size = size; }
String id() { return id; }
SpotSize size() { return size; }
boolean canFit(Vehicle v) { return !occupied && size.canHold(v.size()); }
void occupy() {
if (occupied) throw new IllegalStateException("spot " + id + " is taken");
occupied = true;
}
void free() { occupied = false; }
}
class Level {
private final List<Spot> spots = new ArrayList<>();
void add(Spot spot) { spots.add(spot); }
Spot findSpotFor(Vehicle v) {
for (Spot s : spots) {
if (s.canFit(v)) return s;
}
return null;
}
}
class Ticket {
private final Vehicle vehicle;
private final Spot spot;
private final int entryMinute;
private boolean used;
Ticket(Vehicle vehicle, Spot spot, int entryMinute) {
this.vehicle = vehicle; this.spot = spot; this.entryMinute = entryMinute;
}
Vehicle vehicle() { return vehicle; }
Spot spot() { return spot; }
int minutesParked(int now) { return now - entryMinute; }
void markUsed() {
if (used) throw new IllegalStateException("ticket already used");
used = true;
}
}
class Garage {
private final List<Level> levels = new ArrayList<>();
private final FeePolicy fees;
Garage(FeePolicy fees) { this.fees = fees; }
void addLevel(Level level) { levels.add(level); }
Ticket park(Vehicle v, int now) {
for (Level level : levels) {
Spot spot = level.findSpotFor(v);
if (spot != null) {
spot.occupy();
return new Ticket(v, spot, now);
}
}
throw new IllegalStateException("no free spot for " + v.plate());
}
int leave(Ticket t, int now) {
t.markUsed();
t.spot().free();
return fees.fee(t.minutesParked(now), t.spot().size());
}
}FeePolicy and HourlyFeePolicy are as in step 5. Times are minutes since the garage opened, a shortcut that a date-time class would replace in a real project. Now a main that builds one level with a small spot G1 and a large spot G2 and runs the scenario:
Ticket car = garage.park(new Vehicle("CAR-1", SpotSize.SMALL), 0);
Ticket van = garage.park(new Vehicle("VAN-1", SpotSize.LARGE), 10);
// a third vehicle, no free spot
System.out.println("fee " + garage.leave(car, 130));
// the same ticket again
// a new car arrives at minute 150CAR-1 in G1
VAN-1 in G2
no free spot for CAR-2
fee 6
ticket already used
CAR-2 in G1Read the output as a story. The car takes the small spot; the van the large one; a second car finds none and is refused (the failure scenario). The first car leaves after 130 minutes: three started hours at 2 is 6. Reusing the ticket is rejected, and the freed spot G1 is reused by the next car. Notice what Garage does not contain: no size comparison, no rate numbers, no ticket flag arithmetic.
Step 7: a three-line rationale
Decisions. One
Vehicleclass with aSpotSizeenum, rejectingCar/Trucksubclasses, because vehicles differ only by size; cost: if trucks get special rules, I must revisit. Fees live in aFeePolicyinterface, rejecting anifchain inGarage, because rates change for business reasons; cost: one more type. Risk.2 + ordinal()couples the rate to the enum order and cannot express arbitrary rates.Garage.parkreturns after the first level with a free spot, which is a simple strategy, not an optimal one. Next. Per-spot-size rate table; spots on several levels; a date-time clock.
Examples
Example 1: an enum that owns its comparison (simple)
SpotSize.LARGE.canHold(SpotSize.SMALL) is true, and SpotSize.SMALL.canHold(SpotSize.LARGE) is false. The ordering rule lives in the one place that defines the sizes. Spot.canFit asks the size instead of comparing numbers itself.
Example 2: the god class forming (practical)
Before the fix, Garage had pricing in leave. Counting what Garage would need to change for three plausible requests (weekend rate, daily maximum, free first half hour): each edits leave, which also frees spots and closes tickets. After the extraction, all three edit only HourlyFeePolicy or a new FeePolicy, and Garage is untouched. The design pays for itself the first time a rate changes.
Example 3: a failing scenario guides a design question (realistic)
The output no free spot for CAR-2 shows the failure path works but raises a design question: should the garage throw, or return an empty result? We chose an exception for the demo. The alternative, returning no ticket, forces every caller to check; the cost of an exception is that callers must catch it. Whatever you choose in the lab, say so in your rationale.
Backend Insight
A parking garage looks like a toy, but its shape is typical of backend work: a coordinator (Garage), entities with rules (Spot, Ticket), and a rule that varies by business decision (FeePolicy). Real systems replace the fee interface with a configurable pricing service, and the minutes with timestamps. The reasoning stays the same: keep rules next to their data, and keep the parts that change for business reasons behind small interfaces.
Common Mistakes
Mistake 1: Starting with Car, Truck, and Motorbike subclasses
If they differ only by size, an enum value is enough. Add subclasses when behavior differs.
Mistake 2: Putting fee rules in Garage
It looks natural because leave is where the fee is needed. Needing a result is not the same as owning the rule.
Mistake 3: Comparing sizes outside the enum
if (spot.size().ordinal() >= vehicle.size().ordinal()) in Garage repeats knowledge of the order. Ask canHold.
Mistake 4: No failure scenario
Test the full-garage and reused-ticket cases, not just the happy path.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Why is Driver not a class in this design?
Show solution A1
A1. Nothing in the requirements needs it: the ticket carries the vehicle and its plate. Adding it would be a noun made into a class too early.
A2. Which class should own the rule "a ticket is used once"? Why?
Show solution A2
A2. Ticket, which holds the flag and can protect the rule in markUsed().
A3. What does SpotSize.MEDIUM.canHold(SpotSize.LARGE) return?
Show solution A3
A3. false. A medium spot cannot hold a large vehicle, because the ordinal of MEDIUM (1) is lower than LARGE (2).
Level B: Application
B1. The garage adds a rule: motorbikes may share one small spot, two per spot. Which class changes, and what must you decide before coding?
Show solution B1
B1. Spot, which currently holds a single occupied flag, would need a count or list of vehicles and a capacity, and canFit would change. Decide first whether a motorbike is a separate size or has a "shares a spot" property, and whether one ticket per bike is issued.
B2. Write the FeePolicy implementation FlatFeePolicy that charges 5 per started hour regardless of size. What does Garage need to change?
Show solution B2
B2.
class FlatFeePolicy implements FeePolicy {
public int fee(int minutes, SpotSize size) {
return ((minutes + 59) / 60) * 5;
}
}Garage changes only where it is built: new Garage(new FlatFeePolicy()).
B3. Add a "first 15 minutes free" rule. Which classes do you edit in the god-class version, and which in the policy version?
Show solution B3
B3. In the god-class version you edit Garage.leave. In the policy version you edit only the FeePolicy implementation, for example subtracting 15 from the minutes before rounding (but not below zero).
Level C: Challenge
C1. Add a daily maximum fee of 20 as a new FeePolicy that wraps another policy and caps the result. Why does this need no change to Garage?
Show solution C1
C1. A CappedFeePolicy holds another FeePolicy, calls it, and returns the smaller of the result and 20. Garage only calls fees.fee(...), so it cannot tell the difference. This is composition behind an interface, as in BEG-022 and BEG-024.
C2. Write a half-page rationale (five headings) for your own garage design with at least one rejected alternative that is different from the ones above.
Show solution C2
C2. A good answer has a problem, the classes, two decisions each with a rejected alternative and a cost, one risk, and next steps. For example, a rejected alternative could be storing the free-spot count in Level instead of searching.
Mini Challenge
Extend the design with reserved spots: a spot can be reserved for a named vehicle, and only that vehicle can park there. In a short note:
- Decide which class owns the reservation and why.
- Name the invariant and where it is guarded.
- Describe one normal and one failure scenario.
- State one rejected alternative with its cost.
Check your work: Garage does not compare plates itself.
Summary & Next Lesson
Key takeaways
- Start from the text: nouns, verbs, and rules, then sort them into classes, fields, enums, and nothing.
- A value that varies only in data (size) is an enum or a field, not a hierarchy.
- Give each behavior to the class with the information:
Spot.canFit,Ticket.markUsed. - When pricing appears in the coordinator, extract it behind an interface before it grows.
- A normal and a failure scenario, with real output, and a short rationale complete the design.
Next lesson
This is the last lesson of the module. The next step is the lab, "Parking garage", where you model, implement, explain, and peer-review your own design. After the module, the course continues with the next module in the roadmap.
Self-check
- Why one
Vehicleclass instead ofCarandTruck? - Where does
canFitlive and why? - What sign told us
Garagewas becoming a god class? - Why is
FeePolicyan interface? - What does the "ticket already used" output demonstrate?
Show answers
- They differ only by size, so one field (an enum) is enough.
- In
Spot, which knows its size and whether it is occupied. - It failed the "and" test: parking, tickets, and pricing in one class, with pricing likely to change.
- Fee rules vary for business reasons; callers can then use a new policy without change.
- The ticket guards its own invariant (one use), and
Garagesimply asks it.
References
- Oracle, Enum Types (The Java Tutorials). Supports using an enum for a fixed set of constants and enums having fields and methods. The use of
ordinal()for comparison is our own reasoning, confirmed by the compiled run. - Wikipedia, God object. Supports the signs of a class that grows to hold unrelated responsibilities and the fix of splitting by responsibility.
- Wikipedia, GRASP (object-oriented design)). Supports assigning a responsibility to the class that has the information.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.