Problem definition
A parking lot is a designated area for parking vehicles and is a feature found in almost all popular venues such as shopping malls, sports stadiums, offices, etc. In a parking lot, there are a fixed number of parking spots available for different types of vehicles. Each of these spots is charged according to the time the vehicle has been parked in the parking lot. The parking time is tracked with a parking ticket issued to the vehicle at the entrance of the parking lot. Once the vehicle is ready to exit, it can either pay at the automated exit panel or to the parking agent at the exit using a card or cash payment method.
System Requirements
We will focus on the following set of requirements while designing the parking lot:
The parking lot should have multiple floors where customers can park their cars.
The parking lot should have multiple entry and exit points.
Customers can collect a parking ticket from the entry points and can pay the parking fee at the exit points on their way out.
Customers can pay the tickets at the automated exit panel or to the parking attendant.
Customers can pay via both cash and credit cards.
Customers should also be able to pay the parking fee at the customer’s info portal on each floor. If the customer has paid at the info portal, they don’t have to pay at the exit.
The system should not allow more vehicles than the maximum capacity of the parking lot. If the parking is full, the system should be able to show a message at the entrance panel and on the parking display board on the ground floor.
Each parking floor will have many parking spots. The system should support multiple types of parking spots such as Compact, Large, Handicapped, Motorcycle, etc.
The Parking lot should have some parking spots specified for electric cars. These spots should have an electric panel through which customers can pay and charge their vehicles.
The system should support parking for different types of vehicles like car, truck, van, motorcycle, etc.
Each parking floor should have a display board showing any free parking spot for each spot type.
The system should support a per-hour parking fee model. For example, customers have to pay $4 for the first hour, $3.5 for the second and third hours, and $2.5 for all the remaining hours.
Use case diagram
Here are the main Actors in our system:
Admin: Mainly responsible for adding and modifying parking floors, parking spots, entrance, and exit panels, adding/removing parking attendants, etc.
Customer: All customers can get a parking ticket and pay for it.
Parking attendant: Parking attendants can do all the activities on the customer’s behalf, and can take cash for ticket payment.
System: To display messages on different info panels, as well as assigning and removing a vehicle from a parking spot.
Here are the top use cases for Parking Lot:
Add/Remove/Edit parking floor: To add, remove or modify a parking floor from the system. Each floor can have its own display board to show free parking spots.
Add/Remove/Edit parking spot: To add, remove or modify a parking spot on a parking floor.
Add/Remove a parking attendant: To add or remove a parking attendant from the system.
Take ticket: To provide customers with a new parking ticket when entering the parking lot.
Scan ticket: To scan a ticket to find out the total charge.
Credit card payment: To pay the ticket fee with credit card.
Cash payment: To pay the parking ticket through cash.
Add/Modify parking rate: To allow admin to add or modify the hourly parking rate.
Step 1: Identify the Core Objects (Classes)
"First, I'll carefully read through the requirements and identify the nouns. These often translate to our core objects or classes."
Core objects identified:
- ParkingLot
- Floor
- ParkingSpot
- Vehicle
- Ticket
- EntrancePanel
- ExitPanel
- CustomerInfoPortal
- ParkingAttendant
- Payment
- ElectricPanel
- DisplayBoard
Step 2: Analyze Relationships Between Objects
"Now, I'll think about how these objects relate to each other."
- ParkingLot has multiple Floors
- Floor has multiple ParkingSpots
- Floor has a DisplayBoard
- ParkingLot has EntrancePanels and ExitPanels
- Floor has CustomerInfoPortals
- Ticket is associated with a Vehicle and a ParkingSpot
- Payment is associated with a Ticket
- ElectricPanel is associated with certain ParkingSpots
Step 3: Identify Attributes for Each Class
"Let's think about the characteristics each class should have."
-
ParkingLot
- name
- address
- floors (List)
- entrancePanels (List)
- exitPanels (List)
- maxCapacity
-
Floor
- floorNumber
- parkingSpots (List)
- displayBoard
- customerInfoPortals (List)
-
ParkingSpot
- spotNumber
- type (enum: Compact, Large, Handicapped, Motorcycle, Electric)
- isOccupied
- vehicle (nullable)
-
Vehicle
- licenseNumber
- type (enum: Car, Truck, Van, Motorcycle)
-
Ticket
- ticketNumber
- issueTime
- paymentTime (nullable)
- vehicle
- parkingSpot
- paymentStatus (enum: Unpaid, Paid)
-
EntrancePanel
- id
-
ExitPanel
- id
-
CustomerInfoPortal
- id
-
ParkingAttendant
- id
- name
-
Payment
- amount
- paymentTime
- paymentMethod (enum: Cash, CreditCard)
-
ElectricPanel
- id
- associatedParkingSpot
-
DisplayBoard
- id
- freeSpotsCounts (Dictionary)
Step 4: Identify Methods (Behaviors) for Each Class
"Now, let's think about what actions each object can perform or what can be done to it."
-
ParkingLot
- isFull()
- addFloor(Floor)
- getAvailableSpot(VehicleType)
-
Floor
- addParkingSpot(ParkingSpot)
- updateDisplayBoard()
- getAvailableSpot(VehicleType)
-
ParkingSpot
- occupy(Vehicle)
- vacate()
-
Vehicle
- (No specific methods, mainly used for identification)
-
Ticket
- calculateFee()
- markAsPaid()
-
EntrancePanel
- printTicket(Vehicle)
-
ExitPanel
- processPayment(Ticket, PaymentMethod)
- validateTicket(Ticket)
-
CustomerInfoPortal
- processPayment(Ticket, PaymentMethod)
- getFloorSummary()
-
ParkingAttendant
- processPayment(Ticket, PaymentMethod)
-
Payment
- processPayment()
-
ElectricPanel
- startCharging()
- stopCharging()
- calculateChargingFee()
-
DisplayBoard
- updateFreeSpotsCounts(Dictionary)
Step 5: Identify Abstractions and Inheritance
"Are there any commonalities that we can abstract? Can we use inheritance to simplify our design?"
- We can create an abstract class 'ParkingSpot' with subclasses for each type (Compact, Large, Handicapped, Motorcycle, ElectricSpot)
- We can create an interface 'PaymentProcessor' that EntrancePanel, ExitPanel, and CustomerInfoPortal can implement
- Vehicle can be an abstract class with subclasses Car, Truck, Van, Motorcycle
Step 6: Consider Design Patterns
"Are there any design patterns that could improve our system?"
- Singleton pattern for ParkingLot (assuming one parking lot in the system)
- Factory pattern for creating different types of ParkingSpots and Vehicles
- Strategy pattern for different payment methods
- Observer pattern for updating DisplayBoard when ParkingSpots change state
Step 7: Refine the Design
"Let's refine our design based on these considerations."
public abstract class ParkingSpot {
private String spotNumber;
private boolean isOccupied;
private Vehicle vehicle;
public abstract boolean canFitVehicle(Vehicle vehicle);
public void occupy(Vehicle vehicle) {
this.vehicle = vehicle;
this.isOccupied = true;
}
public void vacate() {
this.vehicle = null;
this.isOccupied = false;
}
}
public class CompactSpot extends ParkingSpot {
public boolean canFitVehicle(Vehicle vehicle) {
return vehicle.getType() == VehicleType.CAR;
}
}
public class LargeSpot extends ParkingSpot {
public boolean canFitVehicle(Vehicle vehicle) {
return vehicle.getType() == VehicleType.CAR || vehicle.getType() == VehicleType.TRUCK;
}
}
public abstract class Vehicle {
private String licenseNumber;
private VehicleType type;
public abstract VehicleType getType();
}
public class Car extends Vehicle {
public VehicleType getType() {
return VehicleType.CAR;
}
}
public interface PaymentProcessor {
public boolean processPayment(Ticket ticket, PaymentMethod method);
}
public class ExitPanel implements PaymentProcessor {
public boolean processPayment(Ticket ticket, PaymentMethod method) {
// Implementation
}
}
public class ParkingLot {
private static ParkingLot instance = null;
private List<Floor> floors;
// Other attributes
private ParkingLot() {
// Private constructor
}
public static ParkingLot getInstance() {
if (instance == null) {
instance = new ParkingLot();
}
return instance;
}
public ParkingSpot getAvailableSpot(Vehicle vehicle) {
// Implementation
}
}
public class ParkingSpotFactory {
public static ParkingSpot createParkingSpot(ParkingSpotType type) {
switch(type) {
case COMPACT:
return new CompactSpot();
case LARGE:
return new LargeSpot();
// Other cases
}
}
}
Step 8: Consider Edge Cases and Error Handling
"What could go wrong? How should we handle errors?"
- What if a vehicle tries to exit without paying?
- What if the parking lot is full when a vehicle tries to enter?
- What if an electric vehicle is parked in a non-electric spot?
We should add appropriate exception handling and error messages for these scenarios.
Step 9: Think About Scalability and Performance
"How will this system scale? Are there any potential performance bottlenecks?"
- The getAvailableSpot method could become slow as the parking lot grows. We might need to implement a more efficient data structure to track available spots.
- We should consider caching frequently accessed data, like the number of available spots per floor.
Step 10: Consider Future Extensions
"How can we make this system easy to extend in the future?"
- Use dependency injection to make it easy to swap out components (like payment processors)
- Design the API in a way that makes it easy to add new features (like valet parking or reserved spots)
This thought process demonstrates how to approach the object-oriented analysis and design of a system like a Parking Lot. It involves iterative thinking, constantly refining the design as new considerations come to light. The end result is a flexible, extensible system that meets the current requirements while being prepared for future changes.
What I provided is indeed part of the Domain Modeling process, which is a crucial step in Object-Oriented Analysis and Design (OOAD). This process helps us understand the problem domain and create a conceptual model before we start coding. Now, let's dive into how this translates to building a system using ASP.NET Core Web API and a layered architecture.
Thought Process of a Professional Software Engineer:
1. Understanding the Relationship between Domain Modeling and Implementation
"The domain model we created serves as a blueprint for our actual implementation. It helps us understand the core concepts and their relationships. Now, we need to translate this conceptual model into a concrete implementation using ASP.NET Core Web API and a layered architecture."
2. Choosing a Layered Architecture
"For our parking lot system, we'll use a common layered architecture:
- Presentation Layer (API Controllers)
- Application Layer (Services)
- Domain Layer (Entities and Interfaces)
- Infrastructure Layer (Data Access, External Services)
This separation of concerns will make our application more maintainable and testable."
3. Setting Up the Project Structure
"Let's create a solution with multiple projects:
- ParkingLot.API (ASP.NET Core Web API project)
- ParkingLot.Application (Class Library)
- ParkingLot.Domain (Class Library)
- ParkingLot.Infrastructure (Class Library)"
4. Implementing the Domain Layer
"We'll start by implementing our domain entities in the ParkingLot.Domain project. These will closely resemble the classes we identified in our domain model."
// ParkingLot.Domain/Entities/ParkingLot.cs
public class ParkingLot
{
public int Id { get; set; }
public string Name { get; set; }
public int Capacity { get; set; }
public List<Floor> Floors { get; set; }
// Other properties and methods
}
// ParkingLot.Domain/Entities/Floor.cs
public class Floor
{
public int Id { get; set; }
public int FloorNumber { get; set; }
public List<ParkingSpot> ParkingSpots { get; set; }
// Other properties and methods
}
// ParkingLot.Domain/Entities/ParkingSpot.cs
public abstract class ParkingSpot
{
public int Id { get; set; }
public string SpotNumber { get; set; }
public bool IsOccupied { get; set; }
public abstract bool CanFitVehicle(Vehicle vehicle);
// Other properties and methods
}
// Additional entity classes...
5. Implementing the Application Layer
"In the ParkingLot.Application project, we'll define interfaces for our services and implement them. These services will contain our business logic."
// ParkingLot.Application/Interfaces/IParkingService.cs
public interface IParkingService
{
Task<Ticket> ParkVehicle(Vehicle vehicle);
Task<Payment> ExitParking(Ticket ticket);
// Other method signatures
}
// ParkingLot.Application/Services/ParkingService.cs
public class ParkingService : IParkingService
{
private readonly IParkingLotRepository _parkingLotRepository;
public ParkingService(IParkingLotRepository parkingLotRepository)
{
_parkingLotRepository = parkingLotRepository;
}
public async Task<Ticket> ParkVehicle(Vehicle vehicle)
{
var parkingLot = await _parkingLotRepository.GetParkingLotAsync();
var availableSpot = parkingLot.FindAvailableSpot(vehicle);
if (availableSpot == null)
throw new NoAvailableSpotException();
availableSpot.OccupySpot(vehicle);
var ticket = new Ticket(vehicle, availableSpot);
await _parkingLotRepository.SaveChangesAsync();
return ticket;
}
// Implement other methods...
}
6. Implementing the Infrastructure Layer
"In the ParkingLot.Infrastructure project, we'll implement our data access logic and any external service integrations."
// ParkingLot.Infrastructure/Data/ParkingLotDbContext.cs
public class ParkingLotDbContext : DbContext
{
public DbSet<ParkingLot> ParkingLots { get; set; }
public DbSet<Floor> Floors { get; set; }
public DbSet<ParkingSpot> ParkingSpots { get; set; }
// Other DbSets...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure entity relationships and constraints
}
}
// ParkingLot.Infrastructure/Repositories/ParkingLotRepository.cs
public class ParkingLotRepository : IParkingLotRepository
{
private readonly ParkingLotDbContext _context;
public ParkingLotRepository(ParkingLotDbContext context)
{
_context = context;
}
public async Task<ParkingLot> GetParkingLotAsync()
{
return await _context.ParkingLots
.Include(pl => pl.Floors)
.ThenInclude(f => f.ParkingSpots)
.FirstOrDefaultAsync();
}
// Implement other methods...
}
7. Implementing the API Layer
"Finally, in our ParkingLot.API project, we'll create controllers that use our application services to handle HTTP requests."
// ParkingLot.API/Controllers/ParkingController.cs
[ApiController]
[Route("api/[controller]")]
public class ParkingController : ControllerBase
{
private readonly IParkingService _parkingService;
public ParkingController(IParkingService parkingService)
{
_parkingService = parkingService;
}
[HttpPost("park")]
public async Task<ActionResult<TicketDto>> ParkVehicle(VehicleDto vehicleDto)
{
var vehicle = new Vehicle(vehicleDto.LicenseNumber, vehicleDto.VehicleType);
var ticket = await _parkingService.ParkVehicle(vehicle);
return Ok(new TicketDto(ticket));
}
[HttpPost("exit")]
public async Task<ActionResult<PaymentDto>> ExitParking(TicketDto ticketDto)
{
var ticket = new Ticket(ticketDto.TicketNumber);
var payment = await _parkingService.ExitParking(ticket);
return Ok(new PaymentDto(payment));
}
// Other action methods...
}
8. Configuring Dependency Injection
"In our Startup.cs file, we'll configure our dependency injection to wire up our services and repositories."
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ParkingLotDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<IParkingLotRepository, ParkingLotRepository>();
services.AddScoped<IParkingService, ParkingService>();
services.AddControllers();
// Other service configurations...
}
9. Implementing Cross-Cutting Concerns
"We should also consider implementing cross-cutting concerns like logging, error handling, and validation."
// Example of a global exception handler middleware
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong: {ex}");
await HandleExceptionAsync(httpContext, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = "Internal Server Error."
}.ToString());
}
}
// In Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware<ExceptionMiddleware>();
// Other middleware configurations...
}
10. Testing
"Throughout this process, we should be writing unit tests for our domain logic, integration tests for our data access, and API tests for our endpoints."
This approach shows how we translate our domain model into a fully-fledged ASP.NET Core Web API application using a layered architecture. The domain model guides our implementation, ensuring that our code accurately represents the problem domain. The layered architecture helps us separate concerns, making the application more maintainable and testable.
Remember, this is an iterative process. As you implement and test, you might discover new insights that cause you to revisit and refine your domain model. This cycle of modeling, implementing, and refining is a key part of professional software development.
Top comments (0)