OOPS! Deep Dive 5: Composition & the Full System
Site Admin · 11/12/2026
Build a Parking Lot, part 5 of 5. Code and practice for the video, in Java and C#.
What you'll learn
- Composition (HAS-A) vs inheritance (IS-A): build big objects from smaller ones
- Write a ParkingLot class that creates and owns its spots, and keeps the open tickets by plate
- enter() finds the first free spot or refuses when the lot is full; exit() frees the spot and returns the fee
- Keep the inside private: no extending a list, no handing out the real list
Download
- parking-lot-dd05-java.zip: Java (lesson code, practice starter, solution)
- parking-lot-dd05-csharp.zip: C# (the same)
The code from the video
Java
Vehicle.java
// Deep Dive 5: unchanged since Deep Dive 4. Vehicle is abstract; each kind brings its own rate and fee rule.
public abstract class Vehicle {
private final String plate;
protected Vehicle(String plate) {
if (plate == null || plate.isBlank())
throw new IllegalArgumentException("plate is required");
this.plate = plate;
}
public String getPlate() { return plate; }
public abstract int hourlyRate(); // no body: each kind must fill it in
public int fee(int hours) { // the usual rule; a child may override it
return hours * hourlyRate();
}
public void describe() {
System.out.println(getClass().getSimpleName() + " " + plate);
}
}ParkingSpot.java
public class ParkingSpot {
private final int number;
private Vehicle parked; // null while free
public ParkingSpot(int number) {
if (number < 1)
throw new IllegalArgumentException("spot number must be 1 or more");
this.number = number;
}
public int getNumber() { return number; }
public boolean isFree() { return parked == null; }
public Ticket park(Vehicle v, int hour) {
if (!isFree())
throw new IllegalStateException("spot " + number + " is taken");
parked = v;
return new Ticket(v, this, hour); // this = this spot
}
public Vehicle leave() {
Vehicle v = parked; // null if the spot was already free: no crash
parked = null;
return v;
}
}Ticket.java
public class Ticket {
private static int nextId = 1; // one counter, shared by all tickets
private final int id;
private final Vehicle vehicle; // has-a Vehicle
private final ParkingSpot spot; // has-a ParkingSpot
private final int entryHour;
public Ticket(Vehicle v, ParkingSpot s, int entryHour) {
if (entryHour < 0 || entryHour > 23)
throw new IllegalArgumentException("hour must be 0 to 23");
this.id = nextId++;
this.vehicle = v;
this.spot = s;
this.entryHour = entryHour;
}
public static int issued() { return nextId - 1; }
public ParkingSpot getSpot() { return spot; }
public int fee(int exitHour) {
int hours = Math.max(1, exitHour - entryHour);
return vehicle.fee(hours); // polymorphism: the vehicle's own rule runs
}
public void print() {
System.out.println("Ticket #" + id + " | " + vehicle.getPlate() + " | spot "
+ spot.getNumber() + " | in at " + entryHour + ":00");
}
}Bike.java
public class Bike extends Vehicle {
public Bike(String plate) {
super(plate);
}
@Override
public int hourlyRate() { return 10; }
}Car.java
public class Car extends Vehicle {
public Car(String plate) {
super(plate);
}
@Override
public int hourlyRate() { return 20; }
}Chargeable.java
// An interface: a promise of what a class can do, not what it is.
public interface Chargeable {
int chargingFee(int kwh);
}ChargingPoint.java
public class ChargingPoint {
// Takes anything Chargeable: it doesn't care what the thing is.
public void charge(Chargeable c, int kwh) {
System.out.println("Charging " + kwh + " kWh = " + c.chargingFee(kwh));
}
}ElectricCar.java
public class ElectricCar extends Car implements Chargeable {
public ElectricCar(String plate) {
super(plate);
}
@Override
public int fee(int hours) {
return super.fee(hours) / 2; // half price: a different rule
}
@Override
public int chargingFee(int kwh) {
return kwh * 3;
}
}ParkingLot.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// Deep Dive 5: composition. The lot is made of spots and keeps the open tickets.
public class ParkingLot {
private final List<ParkingSpot> spots = new ArrayList<>(); // the lot owns its spots
private final Map<String, Ticket> open = new HashMap<>(); // plate -> ticket
public ParkingLot(int size) {
if (size < 1)
throw new IllegalArgumentException("a lot needs at least 1 spot");
for (int i = 1; i <= size; i++)
spots.add(new ParkingSpot(i)); // made here: they live and die with the lot
}
public Ticket enter(Vehicle v, int hour) {
for (ParkingSpot s : spots) {
if (s.isFree()) {
Ticket t = s.park(v, hour);
open.put(v.getPlate(), t);
return t;
}
}
throw new IllegalStateException("lot is full");
}
public int exit(String plate, int hour) {
Ticket t = open.remove(plate);
if (t == null)
throw new IllegalArgumentException("no vehicle with plate " + plate);
t.getSpot().leave(); // free the spot again
return t.fee(hour);
}
public int freeSpots() {
int free = 0;
for (ParkingSpot s : spots)
if (s.isFree()) free++;
return free;
}
}Truck.java
public class Truck extends Vehicle {
private final int axles;
public Truck(String plate, int axles) {
super(plate);
if (axles < 2)
throw new IllegalArgumentException("a truck has at least 2 axles");
this.axles = axles;
}
public int getAxles() { return axles; }
@Override
public int hourlyRate() { return 40; }
@Override
public int fee(int hours) {
return super.fee(Math.max(2, hours)); // at least 2 hours
}
}Main.java
public class Main {
public static void main(String[] args) {
ParkingLot lot = new ParkingLot(4);
lot.enter(new Car("ABC-1234"), 9).print();
lot.enter(new Bike("XYZ-77"), 10).print();
lot.enter(new ElectricCar("EVX-2025"), 11).print();
lot.enter(new Truck("TRK-4040", 3), 11).print();
System.out.println("Free spots: " + lot.freeSpots());
try {
lot.enter(new Car("NEW-5555"), 11);
} catch (IllegalStateException e) {
System.out.println("Refused: " + e.getMessage());
}
System.out.println("ABC-1234 pays " + lot.exit("ABC-1234", 12));
lot.enter(new Car("NEW-5555"), 12).print(); // spot 1 is free again
System.out.println("Free spots: " + lot.freeSpots());
}
}Output:
Ticket #1 | ABC-1234 | spot 1 | in at 9:00
Ticket #2 | XYZ-77 | spot 2 | in at 10:00
Ticket #3 | EVX-2025 | spot 3 | in at 11:00
Ticket #4 | TRK-4040 | spot 4 | in at 11:00
Free spots: 0
Refused: lot is full
ABC-1234 pays 60
Ticket #5 | NEW-5555 | spot 1 | in at 12:00
Free spots: 0C#
Vehicle.cs
// Deep Dive 5: unchanged since Deep Dive 4. Vehicle is abstract; each kind brings its own rate and fee rule.
public abstract class Vehicle
{
private readonly string plate;
protected Vehicle(string plate)
{
if (string.IsNullOrWhiteSpace(plate))
throw new ArgumentException("plate is required");
this.plate = plate;
}
public string Plate => plate;
public abstract int HourlyRate(); // no body: each kind must fill it in
public virtual int Fee(int hours) // the usual rule; virtual: a child may override it
{
return hours * HourlyRate();
}
public void Describe()
{
Console.WriteLine(GetType().Name + " " + plate);
}
}ParkingSpot.cs
public class ParkingSpot
{
private readonly int number;
private Vehicle parked; // null while free
public ParkingSpot(int number)
{
if (number < 1)
throw new ArgumentException("spot number must be 1 or more");
this.number = number;
}
public int Number => number;
public bool IsFree() => parked == null;
public Ticket Park(Vehicle v, int hour)
{
if (!IsFree())
throw new InvalidOperationException("spot " + number + " is taken");
parked = v;
return new Ticket(v, this, hour); // this = this spot
}
public Vehicle Leave()
{
Vehicle v = parked; // null if the spot was already free: no crash
parked = null;
return v;
}
}Ticket.cs
public class Ticket
{
private static int nextId = 1; // one counter, shared by all tickets
private readonly int id;
private readonly Vehicle vehicle; // has-a Vehicle
private readonly ParkingSpot spot; // has-a ParkingSpot
private readonly int entryHour;
public Ticket(Vehicle v, ParkingSpot s, int entryHour)
{
if (entryHour < 0 || entryHour > 23)
throw new ArgumentException("hour must be 0 to 23");
this.id = nextId++;
this.vehicle = v;
this.spot = s;
this.entryHour = entryHour;
}
public static int Issued() => nextId - 1;
public ParkingSpot Spot => spot;
public int Fee(int exitHour)
{
int hours = Math.Max(1, exitHour - entryHour);
return vehicle.Fee(hours); // polymorphism: the vehicle's own rule runs
}
public void Print()
{
Console.WriteLine("Ticket #" + id + " | " + vehicle.Plate + " | spot "
+ spot.Number + " | in at " + entryHour + ":00");
}
}Bike.cs
public class Bike : Vehicle
{
public Bike(string plate) : base(plate)
{
}
public override int HourlyRate() => 10;
}Car.cs
public class Car : Vehicle
{
public Car(string plate) : base(plate)
{
}
public override int HourlyRate() => 20;
}ChargingPoint.cs
public class ChargingPoint
{
// Takes anything IChargeable: it doesn't care what the thing is.
public void Charge(IChargeable c, int kwh)
{
Console.WriteLine("Charging " + kwh + " kWh = " + c.ChargingFee(kwh));
}
}ElectricCar.cs
public class ElectricCar : Car, IChargeable
{
public ElectricCar(string plate) : base(plate)
{
}
public override int Fee(int hours)
{
return base.Fee(hours) / 2; // half price: a different rule
}
public int ChargingFee(int kwh) => kwh * 3;
}IChargeable.cs
// An interface: a promise of what a class can do, not what it is.
public interface IChargeable
{
int ChargingFee(int kwh);
}ParkingLot.cs
// Deep Dive 5: composition. The lot is made of spots and keeps the open tickets.
public class ParkingLot
{
private readonly List<ParkingSpot> spots = new(); // the lot owns its spots
private readonly Dictionary<string, Ticket> open = new(); // plate -> ticket
public ParkingLot(int size)
{
if (size < 1)
throw new ArgumentException("a lot needs at least 1 spot");
for (int i = 1; i <= size; i++)
spots.Add(new ParkingSpot(i)); // made here: they live and die with the lot
}
public Ticket Enter(Vehicle v, int hour)
{
foreach (ParkingSpot s in spots)
{
if (s.IsFree())
{
Ticket t = s.Park(v, hour);
open[v.Plate] = t;
return t;
}
}
throw new InvalidOperationException("lot is full");
}
public int Exit(string plate, int hour)
{
if (!open.Remove(plate, out Ticket t))
throw new ArgumentException("no vehicle with plate " + plate);
t.Spot.Leave(); // free the spot again
return t.Fee(hour);
}
public int FreeSpots()
{
int free = 0;
foreach (ParkingSpot s in spots)
if (s.IsFree()) free++;
return free;
}
}Truck.cs
public class Truck : Vehicle
{
private readonly int axles;
public Truck(string plate, int axles) : base(plate)
{
if (axles < 2)
throw new ArgumentException("a truck has at least 2 axles");
this.axles = axles;
}
public int Axles => axles;
public override int HourlyRate() => 40;
public override int Fee(int hours)
{
return base.Fee(Math.Max(2, hours)); // at least 2 hours
}
}Program.cs
public class Program
{
public static void Main(string[] args)
{
ParkingLot lot = new ParkingLot(4);
lot.Enter(new Car("ABC-1234"), 9).Print();
lot.Enter(new Bike("XYZ-77"), 10).Print();
lot.Enter(new ElectricCar("EVX-2025"), 11).Print();
lot.Enter(new Truck("TRK-4040", 3), 11).Print();
Console.WriteLine("Free spots: " + lot.FreeSpots());
try
{
lot.Enter(new Car("NEW-5555"), 11);
}
catch (InvalidOperationException e)
{
Console.WriteLine("Refused: " + e.Message);
}
Console.WriteLine("ABC-1234 pays " + lot.Exit("ABC-1234", 12));
lot.Enter(new Car("NEW-5555"), 12).Print(); // spot 1 is free again
Console.WriteLine("Free spots: " + lot.FreeSpots());
}
}Output:
Ticket #1 | ABC-1234 | spot 1 | in at 9:00
Ticket #2 | XYZ-77 | spot 2 | in at 10:00
Ticket #3 | EVX-2025 | spot 3 | in at 11:00
Ticket #4 | TRK-4040 | spot 4 | in at 11:00
Free spots: 0
Refused: lot is full
ABC-1234 pays 60
Ticket #5 | NEW-5555 | spot 1 | in at 12:00
Free spots: 0Practice: What did the lot earn today?
Practice (Deep Dive 5): the owner wants to know what the lot earned today.
1. Add a private int field earned to ParkingLot.
2. In exit(), add each fee to earned before returning it.
3. Add a getter earned() that returns the total.
Bonus: in enter(), refuse a plate that is already inside
(throw new IllegalStateException(plate + " is already parked")).
Then uncomment the lines at the end of Main.java.
Expected: XYZ-77 pays 30, then Earned today = 90(Names above are the Java ones; the C# starter below uses the C# names in its comments.)
Start from the lesson code above. Only these files change; the rest are the same as the lesson.
Starter (Java)
ParkingLot.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// Practice (Deep Dive 5): the owner wants to know what the lot earned today.
//
// 1. Add a private int field earned to ParkingLot.
// 2. In exit(), add each fee to earned before returning it.
// 3. Add a getter earned() that returns the total.
// Bonus: in enter(), refuse a plate that is already inside
// (throw new IllegalStateException(plate + " is already parked")).
//
// Then uncomment the lines at the end of Main.java.
// Expected: XYZ-77 pays 30, then Earned today = 90
public class ParkingLot {
private final List<ParkingSpot> spots = new ArrayList<>(); // the lot owns its spots
private final Map<String, Ticket> open = new HashMap<>(); // plate -> ticket
public ParkingLot(int size) {
if (size < 1)
throw new IllegalArgumentException("a lot needs at least 1 spot");
for (int i = 1; i <= size; i++)
spots.add(new ParkingSpot(i)); // made here: they live and die with the lot
}
public Ticket enter(Vehicle v, int hour) {
for (ParkingSpot s : spots) {
if (s.isFree()) {
Ticket t = s.park(v, hour);
open.put(v.getPlate(), t);
return t;
}
}
throw new IllegalStateException("lot is full");
}
public int exit(String plate, int hour) {
Ticket t = open.remove(plate);
if (t == null)
throw new IllegalArgumentException("no vehicle with plate " + plate);
t.getSpot().leave(); // free the spot again
return t.fee(hour);
}
public int freeSpots() {
int free = 0;
for (ParkingSpot s : spots)
if (s.isFree()) free++;
return free;
}
}Main.java
public class Main {
public static void main(String[] args) {
ParkingLot lot = new ParkingLot(4);
lot.enter(new Car("ABC-1234"), 9).print();
lot.enter(new Bike("XYZ-77"), 10).print();
lot.enter(new ElectricCar("EVX-2025"), 11).print();
lot.enter(new Truck("TRK-4040", 3), 11).print();
System.out.println("Free spots: " + lot.freeSpots());
try {
lot.enter(new Car("NEW-5555"), 11);
} catch (IllegalStateException e) {
System.out.println("Refused: " + e.getMessage());
}
System.out.println("ABC-1234 pays " + lot.exit("ABC-1234", 12));
lot.enter(new Car("NEW-5555"), 12).print(); // spot 1 is free again
System.out.println("Free spots: " + lot.freeSpots());
// When earned() is done, uncomment these lines:
// System.out.println("XYZ-77 pays " + lot.exit("XYZ-77", 13));
// System.out.println("Earned today = " + lot.earned());
}
}Starter (C#)
ParkingLot.cs
// Practice (Deep Dive 5): the owner wants to know what the lot earned today.
//
// 1. Add a private int field earned to ParkingLot.
// 2. In Exit(), add each fee to earned before returning it.
// 3. Add a read-only property Earned that returns the total.
// Bonus: in Enter(), refuse a plate that is already inside
// (throw new InvalidOperationException(plate + " is already parked")).
//
// Then uncomment the lines at the end of Program.cs.
// Expected: XYZ-77 pays 30, then Earned today = 90
public class ParkingLot
{
private readonly List<ParkingSpot> spots = new(); // the lot owns its spots
private readonly Dictionary<string, Ticket> open = new(); // plate -> ticket
public ParkingLot(int size)
{
if (size < 1)
throw new ArgumentException("a lot needs at least 1 spot");
for (int i = 1; i <= size; i++)
spots.Add(new ParkingSpot(i)); // made here: they live and die with the lot
}
public Ticket Enter(Vehicle v, int hour)
{
foreach (ParkingSpot s in spots)
{
if (s.IsFree())
{
Ticket t = s.Park(v, hour);
open[v.Plate] = t;
return t;
}
}
throw new InvalidOperationException("lot is full");
}
public int Exit(string plate, int hour)
{
if (!open.Remove(plate, out Ticket t))
throw new ArgumentException("no vehicle with plate " + plate);
t.Spot.Leave(); // free the spot again
return t.Fee(hour);
}
public int FreeSpots()
{
int free = 0;
foreach (ParkingSpot s in spots)
if (s.IsFree()) free++;
return free;
}
}Program.cs
public class Program
{
public static void Main(string[] args)
{
ParkingLot lot = new ParkingLot(4);
lot.Enter(new Car("ABC-1234"), 9).Print();
lot.Enter(new Bike("XYZ-77"), 10).Print();
lot.Enter(new ElectricCar("EVX-2025"), 11).Print();
lot.Enter(new Truck("TRK-4040", 3), 11).Print();
Console.WriteLine("Free spots: " + lot.FreeSpots());
try
{
lot.Enter(new Car("NEW-5555"), 11);
}
catch (InvalidOperationException e)
{
Console.WriteLine("Refused: " + e.Message);
}
Console.WriteLine("ABC-1234 pays " + lot.Exit("ABC-1234", 12));
lot.Enter(new Car("NEW-5555"), 12).Print(); // spot 1 is free again
Console.WriteLine("Free spots: " + lot.FreeSpots());
// When Earned is done, uncomment these lines:
// Console.WriteLine("XYZ-77 pays " + lot.Exit("XYZ-77", 13));
// Console.WriteLine("Earned today = " + lot.Earned);
}
}Solution
Try it yourself first!
Show the Java solution
ParkingLot.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// Deep Dive 5: composition. The lot is made of spots and keeps the open tickets.
public class ParkingLot {
private final List<ParkingSpot> spots = new ArrayList<>(); // the lot owns its spots
private final Map<String, Ticket> open = new HashMap<>(); // plate -> ticket
private int earned; // money taken today
public ParkingLot(int size) {
if (size < 1)
throw new IllegalArgumentException("a lot needs at least 1 spot");
for (int i = 1; i <= size; i++)
spots.add(new ParkingSpot(i)); // made here: they live and die with the lot
}
public Ticket enter(Vehicle v, int hour) {
if (open.containsKey(v.getPlate()))
throw new IllegalStateException(v.getPlate() + " is already parked");
for (ParkingSpot s : spots) {
if (s.isFree()) {
Ticket t = s.park(v, hour);
open.put(v.getPlate(), t);
return t;
}
}
throw new IllegalStateException("lot is full");
}
public int exit(String plate, int hour) {
Ticket t = open.remove(plate);
if (t == null)
throw new IllegalArgumentException("no vehicle with plate " + plate);
t.getSpot().leave(); // free the spot again
int fee = t.fee(hour);
earned += fee;
return fee;
}
public int earned() { return earned; }
public int freeSpots() {
int free = 0;
for (ParkingSpot s : spots)
if (s.isFree()) free++;
return free;
}
}Main.java
public class Main {
public static void main(String[] args) {
ParkingLot lot = new ParkingLot(4);
lot.enter(new Car("ABC-1234"), 9).print();
lot.enter(new Bike("XYZ-77"), 10).print();
lot.enter(new ElectricCar("EVX-2025"), 11).print();
lot.enter(new Truck("TRK-4040", 3), 11).print();
System.out.println("Free spots: " + lot.freeSpots());
try {
lot.enter(new Car("NEW-5555"), 11);
} catch (IllegalStateException e) {
System.out.println("Refused: " + e.getMessage());
}
System.out.println("ABC-1234 pays " + lot.exit("ABC-1234", 12));
lot.enter(new Car("NEW-5555"), 12).print(); // spot 1 is free again
System.out.println("Free spots: " + lot.freeSpots());
System.out.println("XYZ-77 pays " + lot.exit("XYZ-77", 13));
System.out.println("Earned today = " + lot.earned());
}
}Output:
Ticket #1 | ABC-1234 | spot 1 | in at 9:00
Ticket #2 | XYZ-77 | spot 2 | in at 10:00
Ticket #3 | EVX-2025 | spot 3 | in at 11:00
Ticket #4 | TRK-4040 | spot 4 | in at 11:00
Free spots: 0
Refused: lot is full
ABC-1234 pays 60
Ticket #5 | NEW-5555 | spot 1 | in at 12:00
Free spots: 0
XYZ-77 pays 30
Earned today = 90Show the C# solution
ParkingLot.cs
// Deep Dive 5: composition. The lot is made of spots and keeps the open tickets.
public class ParkingLot
{
private readonly List<ParkingSpot> spots = new(); // the lot owns its spots
private readonly Dictionary<string, Ticket> open = new(); // plate -> ticket
private int earned; // money taken today
public ParkingLot(int size)
{
if (size < 1)
throw new ArgumentException("a lot needs at least 1 spot");
for (int i = 1; i <= size; i++)
spots.Add(new ParkingSpot(i)); // made here: they live and die with the lot
}
public Ticket Enter(Vehicle v, int hour)
{
if (open.ContainsKey(v.Plate))
throw new InvalidOperationException(v.Plate + " is already parked");
foreach (ParkingSpot s in spots)
{
if (s.IsFree())
{
Ticket t = s.Park(v, hour);
open[v.Plate] = t;
return t;
}
}
throw new InvalidOperationException("lot is full");
}
public int Exit(string plate, int hour)
{
if (!open.Remove(plate, out Ticket t))
throw new ArgumentException("no vehicle with plate " + plate);
t.Spot.Leave(); // free the spot again
int fee = t.Fee(hour);
earned += fee;
return fee;
}
public int Earned => earned;
public int FreeSpots()
{
int free = 0;
foreach (ParkingSpot s in spots)
if (s.IsFree()) free++;
return free;
}
}Program.cs
public class Program
{
public static void Main(string[] args)
{
ParkingLot lot = new ParkingLot(4);
lot.Enter(new Car("ABC-1234"), 9).Print();
lot.Enter(new Bike("XYZ-77"), 10).Print();
lot.Enter(new ElectricCar("EVX-2025"), 11).Print();
lot.Enter(new Truck("TRK-4040", 3), 11).Print();
Console.WriteLine("Free spots: " + lot.FreeSpots());
try
{
lot.Enter(new Car("NEW-5555"), 11);
}
catch (InvalidOperationException e)
{
Console.WriteLine("Refused: " + e.Message);
}
Console.WriteLine("ABC-1234 pays " + lot.Exit("ABC-1234", 12));
lot.Enter(new Car("NEW-5555"), 12).Print(); // spot 1 is free again
Console.WriteLine("Free spots: " + lot.FreeSpots());
Console.WriteLine("XYZ-77 pays " + lot.Exit("XYZ-77", 13));
Console.WriteLine("Earned today = " + lot.Earned);
}
}Output:
Ticket #1 | ABC-1234 | spot 1 | in at 9:00
Ticket #2 | XYZ-77 | spot 2 | in at 10:00
Ticket #3 | EVX-2025 | spot 3 | in at 11:00
Ticket #4 | TRK-4040 | spot 4 | in at 11:00
Free spots: 0
Refused: lot is full
ABC-1234 pays 60
Ticket #5 | NEW-5555 | spot 1 | in at 12:00
Free spots: 0
XYZ-77 pays 30
Earned today = 90How to run
- Java (17 or newer): in the folder,
javac -d out *.javathenjava -cp out Main - C# (.NET SDK 9 or newer): in the folder,
dotnet run
Previous: Deep Dive 4: Polymorphism, Abstraction & Interfaces
Comments
Loading comments…