10
\$\begingroup\$

I have tried to design the parking lot problem. Here is the problem statement.

Design a parking lot system where Admin can look up all the available spots for parking, He should be able to choose both randomly as well as orderly Using the register number and the type of the vehicle he should be able to issue the parking ticket During the checkout, the cost should be calculated as per length of the time the vehicle stayed inside the parking lot Each slots have a defined capacity and each vehicle has defined space required.

Admin.java

public class Admin {

    private final ParkingLot parkingLot = new ParkingLot("My parking space");
    private final ParkingTicketPrinter parkingTicketPrinter = new ParkingTicketPrinter();

    private final List<Ticket> liveTickets = new ArrayList<>();

    public List<Slot> getAvailableSlots() {

        final List<Slot> result = new ArrayList<>();

        for (Floor floor : getFloors()) {
            for (Slot slot : floor.getSlots()) {
                if (!slot.isOccupied())
                    result.add(slot);
            }
        }
        return result;
    }

    private List<Floor> getFloors() {
        return parkingLot.getFloors();
    }

    private Slot pickRandomSlot() {
        final List<Slot> availableSlots = getAvailableSlots();
        final int randomNum = ThreadLocalRandom.current().nextInt(1, availableSlots.size());
        return availableSlots.get(randomNum);
    }

    private void orderFloors() {
        List<Floor> floors = parkingLot.getFloors();
        Collections.sort(floors);
    }

    public void issueParkingTicket(boolean randomSlotOk, String registerNumber, VehicleTypes type) {
        final Slot slot = randomSlotOk ? pickRandomSlot() : getNearestSlot();
        Ticket ticket = new Ticket(slot,registerNumber, type);
        liveTickets.add(ticket);
        parkingTicketPrinter.printTicket(ticket);
        slot.setOccupied(true);
    }

    public void collectAmount(Ticket ticket) {
        liveTickets.remove(ticket);
        double cost = ticket.getCost();
        System.out.println("Amount to be collected "+ cost);
    }

    private Slot getNearestSlot() {
        orderFloors();
        final List<Slot> availableSlots = getAvailableSlots();
        for (Slot slot : availableSlots) {
            if (!slot.isOccupied())
                return slot;
        }
        return null;
    }

    public List<String> getLiveTicketIds() {
        List<String> result = new ArrayList<>();
        for (Ticket liveTicket : liveTickets) result.add(liveTicket.getTicketNumber());
        return result;
    }

    public void addFloor(long floorNumber) {
        parkingLot.addFloor(new Floor(floorNumber));
    }
}

Ticket related classes

public class Ticket {

    private String ticketNumber;
    private Slot slot;
    private Vehicle vehicle;
    private CostCalculator costCalculator;
    private TicketHelper ticketHelper = new TicketHelper();
    private long inTime = System.currentTimeMillis();
    private static AtomicLong atomicLong = new AtomicLong();

    public Ticket(Slot slot, String registerNumber, VehicleTypes types) {
        this.ticketNumber = String.valueOf(atomicLong.get());
        this.slot = slot;
        this.vehicle = ticketHelper.createVehicle(registerNumber,types);
        this.costCalculator = ticketHelper.getCostCalculator(types);
    }

    public String getTicketNumber() {
        return ticketNumber;
    }

    public String getPrintableString() {
        return slot.getInfo() + "\n" + vehicle.getInfo();
    }

    public double getCost() {
        return costCalculator.getCost(inTime);
    }
}

public class TicketHelper {

    public Vehicle createVehicle(String registerNumber, VehicleTypes type) {
        if (type == VehicleTypes.Car) {
            return new Car(registerNumber);
        } else if (type == VehicleTypes.Bus) {
            return new Bus(registerNumber);
        }
        return null;
    }

    public CostCalculator getCostCalculator(VehicleTypes types) {
        if (types == VehicleTypes.Car) {
            return new CarCostCalculator();
        } else if (types == VehicleTypes.Bus) {
            return new BusCostCalculator();
        }
        return null;
    }
}

Vehicle classes

public class Bus extends Vehicle {

    private int neededSpace = 3;

    public Bus(String registerNumber) {
        super(registerNumber);
    }

    public int getNeededSpace() {
        return neededSpace;
    }

    public String getInfo() {
        return "Bus" + "\n" + " registernumber " + getRegisterNumber();
    }
}


public class Car extends Vehicle {

    private int neededSpace = 2;

    public Car(String registerNumber) {
        super(registerNumber);
    }

    public int getNeededSpace() {
        return neededSpace;
    }

    public String getInfo() {
        return "Car " + "\n" + " registernumber " + getRegisterNumber();
    }
}


public abstract class Vehicle {
    private String registerNumber;

    public Vehicle(String registerNumber) {
        this.registerNumber = registerNumber;
    }

    public String getRegisterNumber() {
        return registerNumber;
    }

    abstract public String getInfo();
}


public enum VehicleTypes {
    Car, Bus, TwoWheeler
}

Cost logic

public interface CostCalculator {
    double getCost(long inTime);
}


public class CarCostCalculator implements CostCalculator {
    @Override    
    public double getCost(long inTime) {
        return (System.currentTimeMillis() - inTime) * 0.6;
    }
}


public class BusCostCalculator implements CostCalculator {
    @Override    
    public double getCost(long inTime) {
        return (System.currentTimeMillis() - inTime) * 0.4;
    }
}

Inventory logic

public class Floor implements Comparable<Floor>{

    private long floorNumber;
    private List<Slot> slots = new ArrayList<>();
    private long occupiedCount;

    public List<Slot> getSlots() {
        return slots;
    }

    public Floor(long floorNumber) {
        this.floorNumber = floorNumber;
    }

    public long getOccupiedCount() {
        return occupiedCount;
    }

    public long getFreeSlotCount() {
        return slots.size() - occupiedCount;
    }

    public boolean isAvailable() {
        return getFreeSlotCount() > 0;
    }

    public long getFloorNumber() {
        return floorNumber;
    }

    public void setSlots(List<Slot> slots) {
        this.slots = slots;
    }

    public void addSlot(Slot slot) {
        slots.add(slot);
    }

    public void removeSlot(Slot slot) {
        slots.remove(slot);
    }

    @Override    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Floor floor = (Floor) o;

        return floorNumber == floor.floorNumber;

    }

    @Override    public int hashCode() {
        return (int) (floorNumber ^ (floorNumber >>> 32));
    }

    @Override    public int compareTo(Floor otherFloor) {
        if (otherFloor.getFloorNumber() > getFloorNumber())
            return 1;
        else if (otherFloor.getFloorNumber() < getFloorNumber())
            return -1;
        return 0;
    }
}


public class ParkingLot {

    private String parkingLotName;
    private List<Floor> floors = new ArrayList<>();

    public ParkingLot(String parkingLotName) {
        this.parkingLotName = parkingLotName;
    }

    public String getParkingLotName() {
        return parkingLotName;
    }

    public List<Floor> getFloors() {
        return floors;
    }

    public void addFloor(Floor floor) {
        floors.add(floor);
    }
    public void deleteFloor(Floor floor) {
        floors.remove(floor);
    }
}


public class Slot {
    private long slotNumber;
    private boolean isOccupied;
    private int capacity;

    public Slot(long slotNumber, int capacity) {
        this.slotNumber = slotNumber;
        this.capacity = capacity;
    }

    public void setOccupied(boolean occupied) {
        isOccupied = occupied;
    }

    public boolean isOccupied() {
        return isOccupied;
    }

    public long getSlotNumber() {
        return slotNumber;
    }

    public int getCapacity() {
        return capacity;
    }

    public String getInfo() {
        return "Slot{" +
                "slotNumber=" + slotNumber +
                ", capacity=" + capacity +
                '}';
    }
}

Printing logic

public class ParkingTicketPrinter {
    private PrintWriter printWriter ;

    public ParkingTicketPrinter() {
        try {
            this.printWriter = new PrintWriter("");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    public void printTicket(Ticket ticket) {
        printWriter.write(ticket.getPrintableString());
    }
}

This is the best I can come up with for now. Please point out if there is any major design flaws or the better way to do the things I just did.

\$\endgroup\$

2 Answers 2

13
\$\begingroup\$

Thanks for sharing your code!

OOP doesn't mean to "split up" code into random classes.

The ultimate goal of this is to reduce code duplication, improve readability and support reuse as well as extending the code.

Doing OOP means that you follow certain principles which are (amongst others):

  • information hiding / encapsulation
  • single responsibility
  • separation of concerns
  • KISS (Keep it simple (and) stupid.)
  • DRY (Don't repeat yourself.)
  • "Tell! Don't ask."
  • Law of demeter ("Don't talk to strangers!")

Your code uses lots of classes but fails to use OOP to implement the business logic.

In particulate the Tell! Don't ask. principle is not applied for slot management. For each request you iterate over the individual parking slots to check availability.

My understanding of an OOP approach is that I have a collection of free slots and another collection of occupied slots. The process that handles the slot occupation should move the slots from one collection to another.

This way I don't need to do any loop when looking for a free slot. And when I search a certain occupied slot I do not need to handle free slots at all. And since the fastest way to do something is not doing it this will speed up the slot management.
Yes, performance should not be a major concern of a design, but here it comes for free...


I would like to see your implementation because you are making a lot of sense. – CodeYogi

I did not alalyse any detail out of your code but this is what I understood and implemented:

  • the parking lot is organized in slots (on levels which I ignored).
  • the slots have different size.
  • a slot can take at most 2 vehicles
  • a slot cannot take a vehicle when its capacity is exceeded
  • the price of a ticket depends on the vehicles size an the duration on the parking lot.

These are the classes I created:

public class Vehicle {
    private final int id;
    private final int spacesNeeded; 
    private final double costFactor;
    private String vehicleType;
    public Vehicle(String vehicleType, int id, int spacesNeeded, double costFactor) {
        super();
        this.vehicleType = vehicleType;
        this.id = id;
        this.spacesNeeded = spacesNeeded;
        this.costFactor = costFactor;
    }
    public int getSpacesNeeded() {
        return spacesNeeded;
    }
    public double getCostFactor() {
        return costFactor;
    }
    @Override
    public String toString() {
        return String.format("Vehicle [vehicleType=%s, id=%d, spacesNeeded=%d, costFactor=%.2f]", vehicleType, id,
                spacesNeeded, costFactor);
    }   
}

There are no subclasses to Vehicle because the Vehicle types in your approach have no different behavior but only different configuration.


public class CostCalculator {
    public double getCost(long parkingDuration, double costFactor) {
        return parkingDuration * costFactor;
    }
}

This class also does not need subclasses for the same reason (no different behavior)


public class Ticket {

    private final long startTime = System.currentTimeMillis();
    private final Vehicle vehicle;

    public Ticket(Vehicle vehicle) {
        super();
        this.vehicle = vehicle;
    }
    public long calcualteParkingDuration(){
        return System.currentTimeMillis() - startTime;
    }
    public double calculateCost(CostCalculator calculator){
        return calculator.getCost(calcualteParkingDuration(), vehicle.getCostFactor());
    }

    public Vehicle getVehicle(){
        return vehicle;
    }
}

public class Slot {
    interface CapacityCalculator {
        boolean isCapacityLeft();
    }

    private static final int FREE = 0;
    private static final int FULL = 2;
    private static int idCounter = 0;
    private final int id = ++idCounter;
    private final int capacity;
    private final List<Vehicle> parkingVehicles = new ArrayList<>();

    public Slot(int capacity) {
        this.capacity = capacity;
    }

    public boolean accepts(Vehicle vehicle) {
        return isSlotFree(() -> (capacity >= parkingVehicles.get(0).getSpacesNeeded() + vehicle.getSpacesNeeded()));
    }

    public void addVehicle(Vehicle vehicle) {
        parkingVehicles.add(vehicle);
        System.out.println(String.format("%s parked on slot %d", vehicle, id));
    }

    public boolean isFree() {
        return isSlotFree(() -> (capacity > parkingVehicles.get(0).getSpacesNeeded()));
    }

    private boolean isSlotFree(CapacityCalculator b) {
        switch (parkingVehicles.size()) {
        case FREE:
            return true;
        case FULL:
            return false;
        default:
            return b.isCapacityLeft();
        }
    }

    public void remove(Vehicle vehicle) {
        parkingVehicles.remove(vehicle);
        System.out.println(String.format("%s removed from slot %d", vehicle, id));      
    }

    @Override
    public String toString() {
        return "Slot [id: " + id + ", capacity=" + capacity + ", parkingVehicles=" + parkingVehicles + "]";
    }
}

public class ParkingLot {
    private final Collection<Slot> freeParkingSlots = new HashSet<>();
    private final Collection<Slot> allParkingSlots = new HashSet<>();
    private final Map<Vehicle, Slot> parkingVehicles = new HashMap<>();
    private double income = 0.0;
    private static final CostCalculator CALCULATOR = new CostCalculator();

    public ParkingLot(int numberOfSlots) {
        Random random = new Random();
        for (int i = 0; i < numberOfSlots; i++) {
            allParkingSlots.add(new Slot(1 + random.nextInt(3)));
        }
        // all slots initially free
        freeParkingSlots.addAll(allParkingSlots);
    }

    public Ticket parkVehicle(Vehicle vehicle) {
        Slot targetSlot = freeParkingSlots.stream().filter(p -> p.accepts(vehicle)).findFirst()
                .orElseThrow(() -> new RuntimeException("No free slot for " + vehicle));
        targetSlot.addVehicle(vehicle);
        if (!targetSlot.isFree()) {
            freeParkingSlots.remove(targetSlot);
        }
        parkingVehicles.put(vehicle, targetSlot);
        return new Ticket(vehicle);
    }

    public void unparkVehicle(Ticket ticket) {
        Slot targetSlot = parkingVehicles.remove(ticket.getVehicle());
        targetSlot.remove(ticket.getVehicle());
        freeParkingSlots.add(targetSlot); // set keeps uniqueness
        income += ticket.calculateCost(CALCULATOR);
    }

    @Override
    public String toString() {
        return String.format("ParkingLot [income=%.2f, freeParkingSlots=%d, parkingVehicles=%d]", income, freeParkingSlots.size(),
                parkingVehicles.size());
    }
}

 public class ParkingLotManager {
    private final ParkingLot parkingLot;
    String[] vehicleTypes = { "TwoWheeler", "Car", "Bus" };

    public ParkingLotManager(int numberOfSlots) {
        parkingLot = new ParkingLot(numberOfSlots);
    }

    public static void main(String[] args) {
        int numberOfSlots = Integer.parseInt(args[0]);
        int numberOfIterations = Integer.parseInt(args[1]);

        new ParkingLotManager(numberOfSlots).start(numberOfIterations);
    }

    private void start(int numberOfIterations) {
        List<Ticket> tickets = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < numberOfIterations; i++) {
            int nextInt = random.nextInt(1 + tickets.size());
            if (nextInt > tickets.size() * 2 / 3) {
                parkingLot.unparkVehicle(tickets.remove(random.nextInt(tickets.size())));
            } else {
                try {
                    parkNewVehicle(tickets, i);
                } catch (Exception e) {
                    System.out.println("Vehicle not parked: "+e.getMessage());
                }
            }
            System.out.println(parkingLot);
        }
    }

    private void parkNewVehicle(List<Ticket> tickets, int i) {
        String vehicleType = vehicleTypes[new Random().nextInt(vehicleTypes.length)];
        int spacesNeeded = 1 + Arrays.asList(vehicleTypes).indexOf(vehicleType);
        double costFactor = 0.2 + (0.2 * spacesNeeded);
        tickets.add(parkingLot.parkVehicle(new Vehicle(vehicleType, i, spacesNeeded, costFactor)));
    }
}
\$\endgroup\$
6
  • 1
    \$\begingroup\$ I would like to see your implementation because you are making a lot of sense. \$\endgroup\$
    – CodeYogi
    Commented Jul 23, 2017 at 3:12
  • \$\begingroup\$ @CodeYogi added example to answer. \$\endgroup\$ Commented Jul 23, 2017 at 15:03
  • \$\begingroup\$ @TimothyTruckle how are you managing multiple floors? \$\endgroup\$
    – WitVault
    Commented Nov 6, 2019 at 10:35
  • \$\begingroup\$ @WitVault it depends... for the time being the floor is nothing more than a property of the Slot class. I cannot see any behavior a floor would have. \$\endgroup\$ Commented Nov 6, 2019 at 11:22
  • \$\begingroup\$ @TimothyTruckle also I don't see much use of allParkingSlots. is it necessary? \$\endgroup\$
    – WitVault
    Commented Nov 6, 2019 at 11:33
3
\$\begingroup\$

Admin class

When you go to McDonalds, do you ask for a hamburger or do you go to the kitchen and cook it yourself? Admin.getFloors should ask for the floors, it should not do it. Many methods in this class make this mistake.

override toString

getInfo() is meaningless as a name, first. Second it's returning a formatted string which is better put into toString().

Printing a ticket will essentially be a chain of toString calls.

You do not need abstract public String getInfo(); - override toString.

Miscellaneous

Parkinglot.Name, not Parkinglot.ParkinglotName. Floor.Number, not Floor.FloorNumber.

ParkinglotPrinter try block makes no sense. Java is compiled, if a class file was missing the compiler will tell you.

public Car(String registerNumber) - add spaceNeeded as a parameter. Not all cars are created equal.

There is no point having Ticket.atomicLong because all you do is pass it to TicketHelper. As an integral part of creating a vehicle this should be where you create vehicles.


// in Admin class
private void orderFloors() {
    List<Floor> floors = parkingLot.getFloors();
    Collections.sort(floors);
}

Why have a private Admin.GetFloors() if you don't use it?


 public Vehicle createVehicle(String registerNumber, VehicleTypes type) {
    if (type == VehicleTypes.Car) {
        return new Car(registerNumber);
    } else if (type == VehicleTypes.Bus) {
        return new Bus(registerNumber);
    }
    return null;
}

I'd throw an exception instead of returning null. If a given VehicleType is not implemented that's a show stopper for sure. Now, if you have a default vehicle type that's a different story. - HINT, HINT.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.