Requirements
Determine the different ways the system will be used. This includes main functions the system needs to perform and who will use it.
- Parking spots:
- Motorcycles, Handicap, Compact, Large spots
- Can check if spot available
- Which vehicle is using it
- Levels:
- Each level will have certain number of spots (assume same for each)
- Each spot will be defined by the Parking Spot class
- Show total spots + total available spots
- Return available spot based on vehicle type
- Parking lots:
- Contain multiple levels
- Can return available spots from lowest level to highest level
- Notifies subscribers for newly available spots
- Ticket:
- Show for which vehicle
- Calculate price based on duration and vehicle type
- Payment Processor:
- Have pay functionality based on ticket
- Support multiple payment types (cash, card)
- Drivers:
- Subscribes to notification for available spot
Define Core Objects
Based on the requirements and use cases, identify the main objects of the system...
- Parking Spot
- Level
- Parking lot
- Ticket
- Payment Processor
- Drivers
Analyze Relationships
Determine how these objects will interact with each other to fulfill the use cases...
- When driver arrives to parking lot, will ask for the first available parking spot based on its vehicle type
- Once a spot is found, the spot will be assigned to the driver, and that specific spot on the parking level will be marked as occupied, removing it from the pool
- Each spot will have their own set of constraints like vehicle size, occupied license plate, occupied time, etc...
- When a spot is being occupied, a ticket will be generated for the driver by the parking lot
- When a spot is done being used, the payment processor will generate the total cost based on duration + vehicle type
- Payment will be made based on payment type
- Drivers that may be waiting will be notified of the available spot
Establish Hierarchy
Design inheritance trees where applicable to promote code reuse and polymorphism. This step involves identifying common attributes and behaviors that can be abstracted into parent classes...
- Parking spot will be an abstract base class for each parking spot type
- Parking lots will contain a list of parking spots, with predefined amounts for each spot type
- Parking lots will contain multiple parking lots, which will be instantiated when the parking lot is instantiated
- Tickets will be a single class, I dont see a huge need here for different ticket types - a ticket is a ticket, but cost will be based on driver details
- Payment processor may be an abstract base class that will have multiple subclasses for each payment type (cash/card)
- Drivers could be inherited from a person base class, which would allow other types of people like admins, etc
Design Patterns
Consider using design patterns (e.g., Factory, Singleton, Observer, Strategy) that fit the problem...
- Can use factory pattern for creating the parking spots to have better code maintainability
- Can also use factory pattern for creating the people subsclasses
- Observer pattern for notifying updates to drivers who are waiting for a spot
- Strategy pattern for different payment methods
Define Class Members (write code)
Attributes: For each class, define the attributes (data) it will hold...
Methods: Define the methods (functions) that operate on the attributes. Ensure they align with the object's responsibilities and adhere to the principle of encapsulation.
from abc import ABC, abstractmethod
from enum import Enum
class VehicleSize(Enum):
MOTORCYCLE = 0
COMPACT = 1
LARGE = 2
def can_fit_in(self, spot_size: VehicleSize) -> bool:
return self.value <= spot_size.value
class Vehicle(ABC):
def init(self, license_plate: str, model: str, brand: str):
self.license_plate = license_plate
self.model = model
self.brand = brand
@property
@abstractmethod
def size(self) -> VehicleSize:
pass
class MotorcycleVehicle(Vehicle):
@property
def size(self):
return VehicleSize.MOTORCYCLE
class CompactVehicle(Vehicle):
@property
def size(self):
return VehicleSize.COMPACT
class LargeVehicle(Vehicle):
@property
def size(self):
return VehicleSize.LARGE
class Driver:
def init(self, vehicle: Vehicle, is_handicapped: bool):
self.vehicle = vehicle
self.is_handicapped = is_handicapped
class ParkingSpot(ABC):
def init(self, spot_id: int):
self.spot_id = spot_id
self.is_occupied = False
self.driver: Driver = None
self.is_handicap_only = False
pass
def can_accommodate(self, driver: Driver) -> bool:
if self.is_handicap_only and not driver.is_handicapped:
return False
return driver.vehicle.spot_size.can_fit_in(self.spot_size)
def assign_spot(self, driver: Driver):
if self.is_occupied:
raise Exception('The spot is already taken')
if not self.can_accommodate(driver):
raise Exception('You cannot occupy this spot')
self.driver = driver
self.is_occupied = True
def return_spot(self):
self.is_occupied = False
self.driver = None
class MotorcycleSpot(ParkingSpot):
fee_multiplier: float = 2.0
fee_strategy: ParkingFeeStrategy = MotorcycleFeeStrategy
spot_size: VehicleSize = VehicleSize.MOTORCYCLE
def init(self, spot_id: int):
super().init(spot_id)
class CompactSpot(ParkingSpot):
fee_multiplier: float = 2.5
fee_strategy: ParkingFeeStrategy = CompactFeeStrategy
spot_size: VehicleSize = VehicleSize.COMPACT
def init(self, spot_id: int):
super().init(spot_id)
class LargeSpot(ParkingSpot):
fee_multiplier: float = 3
fee_strategy: ParkingFeeStrategy = LargeFeeStrategy
spot_size: VehicleSize = VehicleSize.LARGE
def init(self, spot_id: int):
super().init(spot_id)
class HandicapSpot(LargeSpot):
fee_multiplier: float = super().fee_multipler / 2
fee_strategy: ParkingFeeStrategy = HandicapFeeStrategy
def init(self, spot_id: int):
super().init(spot_id)
self.is_handicap_only = True
class ParkingLevel:
def init(self, level_number: int, spot_counts: dict[ParkingSpot, int]):
self._spots = dict[VehicleSize, list[ParkingSpot]]
self.level_number = level_number
for parking_spot, cnt in spot_counts.items():
self._spots[parking_spot.spot_size] = [parking_spot(i) for i in range(1, count+1)]
def find_available_spot(self, driver: Driver) -> Optional[ParkingSpot]:
for spot_size, spots in self._spots.items():
for spot in spots:
if spot.is_occupied or not spot.can_accomodate(driver):
continue
return spot
def available_spots(self) -> dict[VehicleSize, int]:
return {
spot_size, sum(1 for spot in spots if not spot.is_occupied)
for spot_size, spots in _spots.items()
}
class ParkingLot:
def init(self, spots_per_level: list[dict[VehicleSize, int]]):
self.levels: dict[int, ParkingLevel] = {}
self.vehicle_directory: dict[str, ParkingSpot] = {}
for i, spot_counts in enumerate(spots_per_level):
self.levels[i] = ParkingLevel(i, spot_counts)
def park_vehicle(self, driver: Driver) -> ParkingTicket:
for level_num, level in self.levels.items():
spot = level.find_available_spot(driver)
if not spot:
continue
if is_assigned := spot.assign_spot(driver):
self.vehicle_directory[driver.vehicle.license_plate] = spot
return ParkingTicket(driver, spot)
raise Exception('No spots found, try again later')
def initiate_exit(self, ticket: ParkingTicket) -> ParkingTicket:
spot = self.vehicle_directory.get(ticket.license_plate)
if not spot:
raise Exception(f'Parking spot not found for license plate: {license_plate}')
if not spot.driver or not spot.driver.vehicle:
raise Exception('Parking spot found but no associated driver or vehicle')
ticket.calculate_fee()
return ticket
def process_exit(self, ticket: ParkingTicket, payment_processor: PaymentProcessor):
if ticket.status == TicketStatus.PAID:
return True
if payment_processor.process_payment(ticket):
ticket.mark_as_paid()
ticket.parking_spot.return_spot()
return True
raise Exception('Could not complete payment, try again')
class TicketStatus(Enum):
ACTIVE = 'active'
PAID = 'paid'
EXPIRED = 'expired'
class ParkingTicket:
def init(self, driver: Driver, spot: ParkingSpot):
self.ticket_id = self._generate_ticket_id()
self.license_plate = driver.vehicle.license_plate
self.spot_id = spot.spot_id
self.fee_strategy = spot.fee_strategy
self.entry_time = datetime.Now()
self.exit_time = None
self.duration = None
self.status = TicketStatus.ACTIVE
self.amount_due = None
self.payment_id = None
def _generate_ticket_id(self):
// some UUID generator - maybe SHA256
pass
def _calculate_duration(self):
if self.exit_time is None:
self.exit_time = datetime.Now()
self.duration = self.exit_time - self.entry_time
return self.duration
def calculate_fee(self):
if not self.is_valid:
raise Exception('Ticket is not valid, cannot calculate fee')
if self.amount_due > 0:
return self.amount_due
if self.duration == 0:
self._calculate_duration()
self.amount_due = self.fee_strategy(self.duration)
return self.amount_due
def mark_as_paid(self, payment_id: int):
self.status = TicketStatus.PAID
self.payment_id = payment_id
def is_valid(self):
return self.status == TicketStatus.ACTIVE
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, ticket: ParkingTicket):
pass
@abstractmethod
def _pay_amount(self, amount_due: float):
pass
class CashPaymentProcessor(PaymentProcessor):
def process_payment(self, ticket: ParkingTicket):
try:
if self._pay_amount(ticket.amount_due):
return True
except Exception as e:
raise e
return False
def _pay_amount(self, amount_due: float):
// call some API
pass
class CreditCardPaymentProcessor(PaymentProcessor):
def process_payment(self, ticket: ParkingTicket):
try:
if self._pay_amount(ticket.amount_due):
return True
except Exception as e:
raise e
return False
def _pay_amount(self, amount_due: float):
// call some API
pass
class ParkingFeeStrategy(ABC):
@abstractclass
def calculate_fee(self, duration: timedelta) -> float:
pass
class CompactFeeStrategy(ParkingFeeStrategy):
def calculate_fee(self, duration: timedelta) -> float:
return CompactSpot.fee_multiplier * duration
class MotorcycleFeeStrategy(ParkingFeeStrategy):
def calculate_fee(self, duration: timedelta) -> float:
return MotorcycleSpot.fee_multiplier * duration
class LargeFeeStrategy(ParkingFeeStrategy):
def calculate_fee(self, duration: timedelta) -> float:
return LargeSpot.fee_multiplier * duration
class HandicapFeeStrategy(LargeFeeStrategy):
def calculate_fee(self, duration: timedelta) -> float:
return HandicapSpot.fee_multiplier * duration
Adhere to SOLID Guidelines
Check and explain whether your design adheres to solid principles (Ask interviewer what SOLID principle is if you can not recall it.)...
Consider Scalability and Flexibility
Explain how your design can handle changes in scale and whether it would be easily to extend with new functionalities...
Create/Explain your diagram(s)
Try creating a class, flow, state and/or sequence diagram using the diagramming tool. Mermaid flow diagrams can be used to represent system use cases. You can ask the interviewer bot to create a starter diagram if unfamiliar with the tool. Briefly explain your diagrams if necessary...
Future improvements
Critically examine your design for any flaws or areas for future improvement...