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(&#39;You cannot occupy this spot&#39;) 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 = LargeSpot.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] = {} self.payment_service = PaymentService() 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(&#39;No spots found, try again later&#39;) 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(&#39;Parking spot found but no associated driver or vehicle&#39;) ticket.calculate_fee() return ticket def process_exit(self, ticket: ParkingTicket, payment_method: string): try: if self.payment_service.process_parking_payment(ticket, payment_method): spot = self.vehicle_directory.get(ticket.license_plate) spot.return_spot() del self.vehicle_directory[ticket.license_plate] return True except Exception as e: raise e raise Exception(&#39;Could not complete payment, try again&#39;) class PaymentService: def init(self): self.payment_processors = { 'cash': CashPaymentProcessor(), 'card': CreditCardPaymentProcessor() } def process_parking_payment(self, ticket: ParkingTicket, payment_method: str): if not ticket.is_valid(): raise ValueError('Invalid ticket') processor = self.payment_processors.get(payment_method) if not processor: raise Exception(&#39;Invalid payment method&#39;) success = processor.process_payment(ticket) if success: self._generate_receipt(ticket) return True return False def _generate_receipt(ticket: ParkingTicket): pass 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.exit_time and self.exit_time &lt; self.entry_time: raise Exception(&#39;Exit time cannot be before entry time&#39;) if self.amount_due &gt; 0: return self.amount_due if self.duration == 0: self._calculate_duration() self.amount_due = self.fee_strategy.calculate_fee(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...