Lesson 22 / 25

From Requirements to Classes

Turn a problem statement into classes, responsibilities and relationships.

A simple, repeatable design process

Object-oriented design is a skill tested in exams and interviews ("design a parking lot", "design a library system"). A repeatable approach: 1. Clarify requirements: who uses the system, what must it do, what is out of scope. 2. Find candidate classes from the nouns (vehicle, parking spot, ticket, floor, payment) and candidate methods from the verbs (park, unpark, pay, find spot). 3. Assign responsibilities: each class should know or do something specific; techniques such as CRC cards (Class, Responsibilities, Collaborators) help. 4. Define relationships: inheritance only for real is-a cases (car, bike and truck as vehicles), composition for whole-part (a floor has spots), and interfaces for variation points (pricing strategies, payment methods). 5. Walk through scenarios (a car arrives when the lot is full) to check the design handles them. 6. Refine with SOLID principles and simplify. Communicate with a short UML diagram and explain trade-offs aloud.

From nouns and verbs to a class model

Requirements become candidate classes and methods, then a refined model with relationships.

A page of text lines on the left, an arrow to scattered word bubbles in the middle, and an arrow to a neat diagram of connected boxes on the right.
Figure 8.1 — Turning requirements into a class design.

A parking lot design sketch

Core classes, responsibilities and variation points.

enum SpotSize { SMALL, MEDIUM, LARGE }

abstract class Vehicle {
    private final String plate;
    protected Vehicle(String plate) { this.plate = plate; }
    abstract SpotSize requiredSize();
    String plate() { return plate; }
}
final class Bike  extends Vehicle { Bike(String p)  { super(p); } SpotSize requiredSize() { return SpotSize.SMALL; } }
final class Car   extends Vehicle { Car(String p)   { super(p); } SpotSize requiredSize() { return SpotSize.MEDIUM; } }
final class Truck extends Vehicle { Truck(String p) { super(p); } SpotSize requiredSize() { return SpotSize.LARGE; } }

final class Spot {
    final String id; final SpotSize size; private Vehicle parked;
    Spot(String id, SpotSize size) { this.id = id; this.size = size; }
    boolean fits(Vehicle v) { return parked == null && size.compareTo(v.requiredSize()) >= 0; }
    void park(Vehicle v) { parked = v; }
    void free() { parked = null; }
}

interface PricingStrategy { long feePaise(java.time.Duration stay, Vehicle v); }   // variation point

final class ParkingLot {
    private final List<Spot> spots;                 // composition: the lot owns its spots
    private final PricingStrategy pricing;
    ParkingLot(List<Spot> spots, PricingStrategy pricing) { this.spots = spots; this.pricing = pricing; }

    Optional<Ticket> park(Vehicle v) {
        return spots.stream().filter(s -> s.fits(v)).findFirst()
                    .map(s -> { s.park(v); return new Ticket(v, s, java.time.Instant.now()); });
    }
}
record Ticket(Vehicle vehicle, Spot spot, java.time.Instant entry) { }

Ask before you draw

In design interviews, spending the first few minutes on requirements (multiple floors? payment types? reservations?) earns more credit than jumping straight into classes, and it prevents redesigning halfway.

Quick check: In the parking lot design, why is pricing modelled as an interface?

  • Interfaces are faster than classes
  • Java requires interfaces for numbers
  • Pricing rules vary, so an interface lets new strategies be added without changing ParkingLot
  • To make Vehicle abstract
Answer

Pricing rules vary, so an interface lets new strategies be added without changing ParkingLot — Variation points behind interfaces follow the Open/Closed and Dependency Inversion principles.