Chapter 11: Case Study - Elevator System Using State Pattern
Series: Low Level Design for .NET Developers | Previous: Chapter 10: Advanced Concurrency & Caching
📖 Introduction
The Elevator System is a classic LLD problem that tests your understanding of:
- State Pattern - Managing behavior based on current state
- Concurrency - Handling multiple requests simultaneously
- Scheduling Algorithms - Optimizing elevator movement
- Event-Driven Design - Reacting to button presses and sensor inputs
- SOLID Principles - Clean, maintainable architecture
In this case study, we'll build a complete elevator system that can handle multiple elevators, floor requests, and optimize movement.
1. Requirements Analysis
1.1 Functional Requirements
- Multiple elevators serving multiple floors
- Users can request elevator from any floor (external button)
- Users can select destination floor inside elevator (internal button)
- Elevator displays current floor and direction
- Doors open/close automatically with safety sensors
- Emergency stop and alarm functionality
- Optimize elevator assignment based on proximity and direction
- Handle overload condition (weight sensor)
- Maintenance mode support
1.2 Non-Functional Requirements
- Low latency for request handling
- Fair scheduling (no starvation)
- Energy efficiency (minimize unnecessary movement)
- Safe handling of concurrent requests
- Audit trail for maintenance
2. Domain Model
2.1 Core Enums
namespace ElevatorSystem.Domain
{
public enum Direction
{
None,
Up,
Down
}
public enum ElevatorState
{
Idle,
Moving,
DoorOpen,
DoorClosing,
Emergency,
Maintenance
}
public enum DoorState
{
Closed,
Opening,
Open,
Closing
}
public enum RequestType
{
External, // Request from floor buttons
Internal // Request from inside elevator
}
public enum ElevatorStatus
{
Operational,
Maintenance,
OutOfService
}
}
2.2 Value Objects
namespace ElevatorSystem.Domain.ValueObjects
{
public class Floor
{
public int Number { get; }
public Floor(int number)
{
if (number < 1 || number > 100)
throw new ArgumentException("Floor must be between 1 and 100");
Number = number;
}
public static implicit operator int(Floor floor) => floor.Number;
public static implicit operator Floor(int number) => new Floor(number);
public override string ToString() => $"Floor {Number}";
public override bool Equals(object obj) => obj is Floor floor && Number == floor.Number;
public override int GetHashCode() => Number.GetHashCode();
}
public class Weight
{
public double Kilograms { get; }
public Weight(double kilograms)
{
if (kilograms < 0)
throw new ArgumentException("Weight cannot be negative");
Kilograms = kilograms;
}
public static bool operator >(Weight a, Weight b) => a.Kilograms > b.Kilograms;
public static bool operator <(Weight a, Weight b) => a.Kilograms < b.Kilograms;
public override string ToString() => $"{Kilograms} kg";
}
public class Speed
{
public double MetersPerSecond { get; }
public Speed(double metersPerSecond)
{
if (metersPerSecond < 0)
throw new ArgumentException("Speed cannot be negative");
MetersPerSecond = metersPerSecond;
}
public TimeSpan TimeToTravelFloors(int floorCount, double floorHeight = 3.0)
{
var distance = floorCount * floorHeight;
var seconds = distance / MetersPerSecond;
return TimeSpan.FromSeconds(seconds);
}
}
}
3. Elevator State Pattern Implementation
3.1 State Interface and Context
namespace ElevatorSystem.Domain.States
{
// State interface
public interface IElevatorState
{
string StateName { get; }
Task EnterAsync(Elevator elevator);
Task ExitAsync(Elevator elevator);
Task OnFloorArrivedAsync(Elevator elevator, Floor floor);
Task OnDoorOpenedAsync(Elevator elevator);
Task OnDoorClosedAsync(Elevator elevator);
Task OnRequestAsync(Elevator elevator, Request request);
Task OnEmergencyAsync(Elevator elevator);
Task OnMaintenanceAsync(Elevator elevator);
Task OnOverloadAsync(Elevator elevator);
}
// Context - Elevator
public class Elevator
{
private IElevatorState _currentState;
private readonly List<Request> _pendingRequests = new();
private readonly object _lock = new object();
// Properties
public int Id { get; }
public Floor CurrentFloor { get; private set; }
public Direction CurrentDirection { get; private set; }
public DoorState DoorState { get; private set; }
public Weight CurrentWeight { get; private set; }
public int MaxCapacity { get; }
public double MaxWeightKg { get; }
public Speed Speed { get; }
public ElevatorStatus Status { get; private set; }
public List<int> FloorStops { get; private set; } = new();
// Events
public event EventHandler<ElevatorEventArgs> FloorArrived;
public event EventHandler<ElevatorEventArgs> DoorOpened;
public event EventHandler<ElevatorEventArgs> DoorClosed;
public event EventHandler<ElevatorEventArgs> EmergencyActivated;
public Elevator(int id, int startFloor = 1, double maxWeightKg = 1000, int maxCapacity = 10, double speedMps = 1.0)
{
Id = id;
CurrentFloor = new Floor(startFloor);
CurrentDirection = Direction.None;
DoorState = DoorState.Closed;
CurrentWeight = new Weight(0);
MaxWeightKg = maxWeightKg;
MaxCapacity = maxCapacity;
Speed = new Speed(speedMps);
Status = ElevatorStatus.Operational;
// Start in Idle state
_currentState = new IdleState();
_currentState.EnterAsync(this).Wait();
}
public void SetState(IElevatorState newState)
{
Console.WriteLine($"Elevator {Id}: {_currentState.StateName} → {newState.StateName}");
_currentState.ExitAsync(this).Wait();
_currentState = newState;
_currentState.EnterAsync(this).Wait();
}
public async Task MoveToFloorAsync(Floor targetFloor)
{
if (targetFloor.Number == CurrentFloor.Number)
{
await _currentState.OnFloorArrivedAsync(this, targetFloor);
return;
}
CurrentDirection = targetFloor.Number > CurrentFloor.Number ? Direction.Up : Direction.Down;
// Simulate movement
var floorsToMove = Math.Abs(targetFloor.Number - CurrentFloor.Number);
var travelTime = Speed.TimeToTravelFloors(floorsToMove);
Console.WriteLine($"Elevator {Id}: Moving {CurrentDirection} from floor {CurrentFloor} to {targetFloor}");
// Simulate passing intermediate floors
for (int step = 1; step <= floorsToMove; step++)
{
var intermediateFloor = CurrentFloor.Number + (CurrentDirection == Direction.Up ? step : -step);
await Task.Delay(travelTime / floorsToMove);
var floor = new Floor(intermediateFloor);
OnFloorArrived(floor);
// Check if there's a pending request at this floor
lock (_lock)
{
if (_pendingRequests.Any(r => r.Floor.Number == floor.Number &&
(r.Type == RequestType.External && r.Direction == CurrentDirection)))
{
Console.WriteLine($"Elevator {Id}: Stopping at floor {floor} due to pending request");
await _currentState.OnFloorArrivedAsync(this, floor);
break;
}
}
}
await _currentState.OnFloorArrivedAsync(this, targetFloor);
}
public void OpenDoors()
{
DoorState = DoorState.Opening;
Console.WriteLine($"Elevator {Id}: Opening doors at floor {CurrentFloor}");
// Simulate door opening time
Task.Delay(1000).Wait();
DoorState = DoorState.Open;
OnDoorOpened();
// Auto-close after delay
Task.Delay(3000).ContinueWith(_ => CloseDoors());
}
public void CloseDoors()
{
DoorState = DoorState.Closing;
Console.WriteLine($"Elevator {Id}: Closing doors");
// Simulate door closing time
Task.Delay(1000).Wait();
DoorState = DoorState.Closed;
OnDoorClosed();
}
public void AddRequest(Request request)
{
lock (_lock)
{
_pendingRequests.Add(request);
Console.WriteLine($"Elevator {Id}: Added request - {request}");
}
_currentState.OnRequestAsync(this, request).Wait();
}
public Request GetNextRequest()
{
lock (_lock)
{
if (!_pendingRequests.Any())
return null;
// Optimize request selection based on current direction
var sameDirectionRequests = _pendingRequests
.Where(r => r.Direction == CurrentDirection || CurrentDirection == Direction.None)
.OrderBy(r => CurrentDirection == Direction.Up
? r.Floor.Number
: -r.Floor.Number)
.ToList();
if (sameDirectionRequests.Any())
{
var request = sameDirectionRequests.First();
_pendingRequests.Remove(request);
return request;
}
// No requests in current direction, take closest
var closest = _pendingRequests
.OrderBy(r => Math.Abs(r.Floor.Number - CurrentFloor.Number))
.First();
_pendingRequests.Remove(closest);
return closest;
}
}
public bool HasPendingRequests() => _pendingRequests.Any();
public void AddWeight(double weight)
{
CurrentWeight = new Weight(CurrentWeight.Kilograms + weight);
if (CurrentWeight.Kilograms > MaxWeightKg)
{
_currentState.OnOverloadAsync(this).Wait();
}
}
public void RemoveWeight(double weight)
{
CurrentWeight = new Weight(Math.Max(0, CurrentWeight.Kilograms - weight));
}
public void ActivateEmergency()
{
_currentState.OnEmergencyAsync(this).Wait();
}
public void EnterMaintenance()
{
_currentState.OnMaintenanceAsync(this).Wait();
}
private void OnFloorArrived(Floor floor)
{
CurrentFloor = floor;
FloorArrived?.Invoke(this, new ElevatorEventArgs { ElevatorId = Id, Floor = floor });
}
private void OnDoorOpened()
{
DoorOpened?.Invoke(this, new ElevatorEventArgs { ElevatorId = Id, Floor = CurrentFloor });
}
private void OnDoorClosed()
{
DoorClosed?.Invoke(this, new ElevatorEventArgs { ElevatorId = Id, Floor = CurrentFloor });
}
}
public class ElevatorEventArgs : EventArgs
{
public int ElevatorId { get; set; }
public Floor Floor { get; set; }
public Direction Direction { get; set; }
public string Message { get; set; }
}
}
3.2 Concrete States
// Idle State - Elevator waiting for requests
public class IdleState : IElevatorState
{
public string StateName => "Idle";
public async Task EnterAsync(Elevator elevator)
{
Console.WriteLine($"Elevator {elevator.Id} is idle at floor {elevator.CurrentFloor}");
await Task.CompletedTask;
}
public async Task ExitAsync(Elevator elevator)
{
await Task.CompletedTask;
}
public async Task OnFloorArrivedAsync(Elevator elevator, Floor floor)
{
// Already at floor
}
public async Task OnDoorOpenedAsync(Elevator elevator)
{
// Not applicable in idle state
}
public async Task OnDoorClosedAsync(Elevator elevator)
{
// Not applicable
}
public async Task OnRequestAsync(Elevator elevator, Request request)
{
Console.WriteLine($"Elevator {elevator.Id}: Received request while idle - {request}");
// Change to moving state and process request
elevator.SetState(new MovingState());
// If request is external, move to that floor
if (request.Type == RequestType.External)
{
await elevator.MoveToFloorAsync(request.Floor);
}
// If request is internal, doors are already open? This shouldn't happen in idle
}
public async Task OnEmergencyAsync(Elevator elevator)
{
elevator.SetState(new EmergencyState());
}
public async Task OnMaintenanceAsync(Elevator elevator)
{
elevator.SetState(new MaintenanceState());
}
public async Task OnOverloadAsync(Elevator elevator)
{
// In idle state, overload can't happen
}
}
// Moving State - Elevator in motion
public class MovingState : IElevatorState
{
public string StateName => "Moving";
public async Task EnterAsync(Elevator elevator)
{
Console.WriteLine($"Elevator {elevator.Id} is moving {elevator.CurrentDirection}");
await Task.CompletedTask;
}
public async Task ExitAsync(Elevator elevator)
{
await Task.CompletedTask;
}
public async Task OnFloorArrivedAsync(Elevator elevator, Floor floor)
{
Console.WriteLine($"Elevator {elevator.Id}: Arrived at floor {floor}");
// Check if this floor has pending requests
var hasStop = elevator.FloorStops.Contains(floor.Number) ||
elevator.HasPendingRequests();
if (hasStop)
{
elevator.SetState(new DoorOpenState());
elevator.OpenDoors();
}
else
{
// Continue moving
var nextRequest = elevator.GetNextRequest();
if (nextRequest != null)
{
await elevator.MoveToFloorAsync(nextRequest.Floor);
}
else
{
elevator.SetState(new IdleState());
}
}
}
public async Task OnDoorOpenedAsync(Elevator elevator)
{
// Not applicable
}
public async Task OnDoorClosedAsync(Elevator elevator)
{
// Not applicable
}
public async Task OnRequestAsync(Elevator elevator, Request request)
{
// Add to pending requests while moving
Console.WriteLine($"Elevator {elevator.Id}: Adding request while moving - {request}");
// If request is in same direction and ahead, add to stops
if (request.Type == RequestType.Internal ||
(request.Direction == elevator.CurrentDirection &&
(elevator.CurrentDirection == Direction.Up && request.Floor.Number > elevator.CurrentFloor.Number) ||
(elevator.CurrentDirection == Direction.Down && request.Floor.Number < elevator.CurrentFloor.Number)))
{
elevator.FloorStops.Add(request.Floor.Number);
}
}
public async Task OnEmergencyAsync(Elevator elevator)
{
// Stop at next floor and enter emergency
elevator.SetState(new EmergencyState());
}
public async Task OnMaintenanceAsync(Elevator elevator)
{
elevator.SetState(new MaintenanceState());
}
public async Task OnOverloadAsync(Elevator elevator)
{
// Overload can't happen while moving
}
}
// Door Open State - Doors are open for boarding
public class DoorOpenState : IElevatorState
{
public string StateName => "Door Open";
public async Task EnterAsync(Elevator elevator)
{
Console.WriteLine($"Elevator {elevator.Id}: Doors open at floor {elevator.CurrentFloor}");
// Process internal requests from passengers
var internalRequests = elevator.GetNextRequest();
while (internalRequests != null && internalRequests.Type == RequestType.Internal)
{
Console.WriteLine($"Elevator {elevator.Id}: Passenger selected floor {internalRequests.Floor}");
internalRequests = elevator.GetNextRequest();
}
}
public async Task ExitAsync(Elevator elevator)
{
await Task.CompletedTask;
}
public async Task OnFloorArrivedAsync(Elevator elevator, Floor floor)
{
// Already at floor
}
public async Task OnDoorOpenedAsync(Elevator elevator)
{
// Already open
}
public async Task OnDoorClosedAsync(Elevator elevator)
{
// Doors closed, move to next request
var nextRequest = elevator.GetNextRequest();
if (nextRequest != null)
{
elevator.SetState(new MovingState());
await elevator.MoveToFloorAsync(nextRequest.Floor);
}
else
{
elevator.SetState(new IdleState());
}
}
public async Task OnRequestAsync(Elevator elevator, Request request)
{
// Add request while doors are open
if (request.Type == RequestType.Internal)
{
// Internal request from inside elevator
elevator.FloorStops.Add(request.Floor.Number);
Console.WriteLine($"Elevator {elevator.Id}: Added internal request to floor {request.Floor}");
}
else
{
// External request - add to pending
elevator.AddRequest(request);
}
}
public async Task OnEmergencyAsync(Elevator elevator)
{
elevator.SetState(new EmergencyState());
}
public async Task OnMaintenanceAsync(Elevator elevator)
{
elevator.SetState(new MaintenanceState());
}
public async Task OnOverloadAsync(Elevator elevator)
{
Console.WriteLine($"Elevator {elevator.Id}: OVERLOAD detected! Weight: {elevator.CurrentWeight.Kilograms}kg");
// Keep doors open until overload is resolved
elevator.SetState(new OverloadState());
}
}
// Overload State - Too much weight
public class OverloadState : IElevatorState
{
public string StateName => "Overload";
public async Task EnterAsync(Elevator elevator)
{
Console.WriteLine($"Elevator {elevator.Id}: OVERLOAD! Please reduce weight.");
// Keep doors open
elevator.DoorState = DoorState.Open;
// Alert and wait for weight reduction
await Task.Delay(1000);
}
public async Task ExitAsync(Elevator elevator)
{
Console.WriteLine($"Elevator {elevator.Id}: Overload resolved. Weight: {elevator.CurrentWeight.Kilograms}kg");
}
public async Task OnFloorArrivedAsync(Elevator elevator, Floor floor) { }
public async Task OnDoorOpenedAsync(Elevator elevator) { }
public async Task OnDoorClosedAsync(Elevator elevator) { }
public async Task OnRequestAsync(Elevator elevator, Request request)
{
// Ignore requests during overload
Console.WriteLine($"Elevator {elevator.Id}: Cannot process requests during overload");
}
public async Task OnEmergencyAsync(Elevator elevator)
{
elevator.SetState(new EmergencyState());
}
public async Task OnMaintenanceAsync(Elevator elevator)
{
elevator.SetState(new MaintenanceState());
}
public async Task OnOverloadAsync(Elevator elevator)
{
// Already in overload
}
}
// Emergency State - Emergency situation
public class EmergencyState : IElevatorState
{
public string StateName => "Emergency";
public async Task EnterAsync(Elevator elevator)
{
Console.WriteLine($"Elevator {elevator.Id}: EMERGENCY MODE ACTIVATED!");
// Stop at nearest floor
await elevator.MoveToFloorAsync(elevator.CurrentFloor);
elevator.OpenDoors();
// Alert monitoring system
elevator.EmergencyActivated?.Invoke(elevator, new ElevatorEventArgs
{
ElevatorId = elevator.Id,
Message = "Emergency activated"
});
}
public async Task ExitAsync(Elevator elevator)
{
Console.WriteLine($"Elevator {elevator.Id}: Exiting emergency mode");
}
public async Task OnFloorArrivedAsync(Elevator elevator, Floor floor) { }
public async Task OnDoorOpenedAsync(Elevator elevator) { }
public async Task OnDoorClosedAsync(Elevator elevator) { }
public async Task OnRequestAsync(Elevator elevator, Request request)
{
// Only accept emergency override
if (request.Type == RequestType.Internal && request.Floor.Number == -1) // Emergency override
{
elevator.SetState(new MovingState());
}
}
public async Task OnEmergencyAsync(Elevator elevator) { }
public async Task OnMaintenanceAsync(Elevator elevator) { }
public async Task OnOverloadAsync(Elevator elevator) { }
}
// Maintenance State - Out of service
public class MaintenanceState : IElevatorState
{
public string StateName => "Maintenance";
public async Task EnterAsync(Elevator elevator)
{
Console.WriteLine($"Elevator {elevator.Id}: MAINTENANCE MODE - Out of service");
elevator.Status = ElevatorStatus.Maintenance;
// Move to maintenance floor if not there
if (elevator.CurrentFloor.Number != 1)
{
await elevator.MoveToFloorAsync(new Floor(1));
}
elevator.DoorState = DoorState.Closed;
}
public async Task ExitAsync(Elevator elevator)
{
Console.WriteLine($"Elevator {elevator.Id}: Exiting maintenance mode");
elevator.Status = ElevatorStatus.Operational;
}
public async Task OnFloorArrivedAsync(Elevator elevator, Floor floor) { }
public async Task OnDoorOpenedAsync(Elevator elevator) { }
public async Task OnDoorClosedAsync(Elevator elevator) { }
public async Task OnRequestAsync(Elevator elevator, Request request)
{
Console.WriteLine($"Elevator {elevator.Id}: Cannot process requests - in maintenance");
}
public async Task OnEmergencyAsync(Elevator elevator)
{
// Emergency override during maintenance
elevator.SetState(new EmergencyState());
}
public async Task OnMaintenanceAsync(Elevator elevator) { }
public async Task OnOverloadAsync(Elevator elevator) { }
}
4. Request and Dispatch System
4.1 Request Models
namespace ElevatorSystem.Domain
{
public class Request
{
public int Id { get; }
public Floor Floor { get; }
public Direction Direction { get; }
public RequestType Type { get; }
public DateTime Timestamp { get; }
private static int _nextId = 1;
public Request(Floor floor, Direction direction, RequestType type)
{
Id = _nextId++;
Floor = floor;
Direction = direction;
Type = type;
Timestamp = DateTime.UtcNow;
}
public static Request ExternalRequest(Floor floor, Direction direction)
=> new Request(floor, direction, RequestType.External);
public static Request InternalRequest(Floor floor)
=> new Request(floor, Direction.None, RequestType.Internal);
public override string ToString()
{
return Type == RequestType.External
? $"External request at {Floor} going {Direction}"
: $"Internal request to {Floor}";
}
}
// Dispatcher - Assigns elevators to requests
public class ElevatorDispatcher
{
private readonly List<Elevator> _elevators;
private readonly object _lock = new object();
public ElevatorDispatcher(List<Elevator> elevators)
{
_elevators = elevators;
}
public Elevator Dispatch(Request request)
{
lock (_lock)
{
var availableElevators = _elevators
.Where(e => e.Status == ElevatorStatus.Operational)
.ToList();
if (!availableElevators.Any())
throw new InvalidOperationException("No operational elevators available");
// For internal requests, return the elevator that made the request
if (request.Type == RequestType.Internal)
{
// This should be handled by the elevator itself
return null;
}
// Find best elevator based on scoring
var bestElevator = availableElevators
.Select(e => new
{
Elevator = e,
Score = CalculateScore(e, request)
})
.OrderBy(x => x.Score)
.First().Elevator;
Console.WriteLine($"Dispatcher: Assigning request {request} to Elevator {bestElevator.Id} (Score: {CalculateScore(bestElevator, request)})");
return bestElevator;
}
}
private int CalculateScore(Elevator elevator, Request request)
{
int score = 0;
var floorDistance = Math.Abs(elevator.CurrentFloor.Number - request.Floor.Number);
// Distance factor (closer is better)
score += floorDistance;
// Direction factor (same direction is better)
if (elevator.CurrentDirection == request.Direction)
score -= 5;
else if (elevator.CurrentDirection == Direction.None)
score -= 2;
else
score += 10;
// Request in path factor
if (elevator.CurrentDirection == Direction.Up &&
request.Direction == Direction.Up &&
request.Floor.Number > elevator.CurrentFloor.Number)
score -= 8;
else if (elevator.CurrentDirection == Direction.Down &&
request.Direction == Direction.Down &&
request.Floor.Number < elevator.CurrentFloor.Number)
score -= 8;
// Load factor (prefer less loaded elevators)
var loadPercentage = elevator.CurrentWeight.Kilograms / elevator.MaxWeightKg;
score += (int)(loadPercentage * 20);
return score;
}
}
}
5. Building and Floor Management
namespace ElevatorSystem.Domain
{
public class Building
{
public string Name { get; }
public int TotalFloors { get; }
public List<FloorPanel> FloorPanels { get; private set; }
public ElevatorDispatcher Dispatcher { get; private set; }
public List<Elevator> Elevators { get; private set; }
public Building(string name, int totalFloors, int elevatorCount)
{
Name = name;
TotalFloors = totalFloors;
Elevators = new List<Elevator>();
FloorPanels = new List<FloorPanel>();
// Create elevators
for (int i = 1; i <= elevatorCount; i++)
{
var elevator = new Elevator(i, 1);
elevator.FloorArrived += OnElevatorFloorArrived;
elevator.DoorOpened += OnElevatorDoorOpened;
elevator.DoorClosed += OnElevatorDoorClosed;
elevator.EmergencyActivated += OnElevatorEmergency;
Elevators.Add(elevator);
}
// Create floor panels
for (int floor = 1; floor <= totalFloors; floor++)
{
var panel = new FloorPanel(new Floor(floor));
panel.UpButtonPressed += OnUpButtonPressed;
panel.DownButtonPressed += OnDownButtonPressed;
FloorPanels.Add(panel);
}
// Create dispatcher
Dispatcher = new ElevatorDispatcher(Elevators);
}
public void CallElevator(Floor floor, Direction direction)
{
var request = Request.ExternalRequest(floor, direction);
var elevator = Dispatcher.Dispatch(request);
elevator?.AddRequest(request);
}
private void OnUpButtonPressed(object sender, FloorEventArgs e)
{
Console.WriteLine($"Building: Up button pressed at {e.Floor}");
CallElevator(e.Floor, Direction.Up);
}
private void OnDownButtonPressed(object sender, FloorEventArgs e)
{
Console.WriteLine($"Building: Down button pressed at {e.Floor}");
CallElevator(e.Floor, Direction.Down);
}
private void OnElevatorFloorArrived(object sender, ElevatorEventArgs e)
{
// Update floor displays
foreach (var panel in FloorPanels)
{
panel.UpdateDisplay($"Elevator {e.ElevatorId} arrived");
}
}
private void OnElevatorDoorOpened(object sender, ElevatorEventArgs e)
{
Console.WriteLine($"Building: Elevator {e.ElevatorId} doors opened at {e.Floor}");
}
private void OnElevatorDoorClosed(object sender, ElevatorEventArgs e)
{
Console.WriteLine($"Building: Elevator {e.ElevatorId} doors closed at {e.Floor}");
}
private void OnElevatorEmergency(object sender, ElevatorEventArgs e)
{
Console.WriteLine($"Building: ALERT! Elevator {e.ElevatorId} emergency at {e.Floor}");
// Send alert to monitoring system
}
public async Task SimulateUsageAsync()
{
var random = new Random();
for (int i = 0; i < 50; i++)
{
var floor = new Floor(random.Next(1, TotalFloors + 1));
var direction = random.Next(0, 2) == 0 ? Direction.Up : Direction.Down;
// Don't go up from top floor or down from bottom floor
if (floor.Number == TotalFloors) direction = Direction.Down;
if (floor.Number == 1) direction = Direction.Up;
CallElevator(floor, direction);
// Random internal requests from elevators
var randomElevator = Elevators[random.Next(Elevators.Count)];
if (randomElevator.Status == ElevatorStatus.Operational)
{
var targetFloor = new Floor(random.Next(1, TotalFloors + 1));
if (targetFloor.Number != randomElevator.CurrentFloor.Number)
{
var internalRequest = Request.InternalRequest(targetFloor);
randomElevator.AddRequest(internalRequest);
}
}
await Task.Delay(random.Next(2000, 5000));
}
}
}
public class FloorPanel
{
public Floor Floor { get; }
public bool UpButtonLit { get; private set; }
public bool DownButtonLit { get; private set; }
public event EventHandler<FloorEventArgs> UpButtonPressed;
public event EventHandler<FloorEventArgs> DownButtonPressed;
public FloorPanel(Floor floor)
{
Floor = floor;
}
public void PressUp()
{
UpButtonLit = true;
UpButtonPressed?.Invoke(this, new FloorEventArgs { Floor = Floor, Direction = Direction.Up });
// Auto-reset after 5 seconds (simulating elevator arrival)
Task.Delay(5000).ContinueWith(_ => UpButtonLit = false);
}
public void PressDown()
{
DownButtonLit = true;
DownButtonPressed?.Invoke(this, new FloorEventArgs { Floor = Floor, Direction = Direction.Down });
Task.Delay(5000).ContinueWith(_ => DownButtonLit = false);
}
public void UpdateDisplay(string message)
{
// Update floor display
Console.WriteLine($"Floor {Floor} Display: {message}");
}
}
public class FloorEventArgs : EventArgs
{
public Floor Floor { get; set; }
public Direction Direction { get; set; }
}
}
6. Control System and Simulation
namespace ElevatorSystem.Control
{
public class ElevatorControlSystem
{
private readonly Building _building;
private readonly ILogger<ElevatorControlSystem> _logger;
private bool _isRunning;
public ElevatorControlSystem(int totalFloors, int elevatorCount, ILogger<ElevatorControlSystem> logger = null)
{
_building = new Building("Main Tower", totalFloors, elevatorCount);
_logger = logger;
}
public async Task StartAsync()
{
_isRunning = true;
_logger?.LogInformation("Elevator Control System started");
// Start monitoring
await MonitorSystemAsync();
}
public async Task StopAsync()
{
_isRunning = false;
_logger?.LogInformation("Elevator Control System stopped");
await Task.CompletedTask;
}
public void CallElevator(int floor, Direction direction)
{
_building.CallElevator(new Floor(floor), direction);
}
public void SelectFloor(int elevatorId, int floor)
{
var elevator = _building.Elevators.FirstOrDefault(e => e.Id == elevatorId);
if (elevator != null)
{
var request = Request.InternalRequest(new Floor(floor));
elevator.AddRequest(request);
}
}
public async Task SimulateRandomUsageAsync(int requestCount, int delayMs = 3000)
{
_logger?.LogInformation("Starting simulation with {RequestCount} requests", requestCount);
var random = new Random();
for (int i = 0; i < requestCount; i++)
{
var floor = random.Next(1, _building.TotalFloors + 1);
var direction = random.Next(0, 2) == 0 ? Direction.Up : Direction.Down;
if (floor == _building.TotalFloors) direction = Direction.Down;
if (floor == 1) direction = Direction.Up;
CallElevator(floor, direction);
// Random internal requests
if (random.Next(0, 2) == 0)
{
var elevatorId = random.Next(1, _building.Elevators.Count + 1);
var targetFloor = random.Next(1, _building.TotalFloors + 1);
SelectFloor(elevatorId, targetFloor);
}
await Task.Delay(random.Next(delayMs / 2, delayMs));
}
_logger?.LogInformation("Simulation completed");
}
public async Task SimulateEmergencyAsync(int elevatorId)
{
var elevator = _building.Elevators.FirstOrDefault(e => e.Id == elevatorId);
if (elevator != null)
{
_logger?.LogWarning("Simulating emergency on elevator {ElevatorId}", elevatorId);
elevator.ActivateEmergency();
}
await Task.CompletedTask;
}
private async Task MonitorSystemAsync()
{
while (_isRunning)
{
await Task.Delay(5000);
var status = GetSystemStatus();
_logger?.LogInformation("System Status: {Status}", status);
}
}
public string GetSystemStatus()
{
var sb = new StringBuilder();
sb.AppendLine($"\n=== Elevator System Status ===");
sb.AppendLine($"Building: {_building.Name}, Floors: {_building.TotalFloors}");
foreach (var elevator in _building.Elevators)
{
sb.AppendLine($" Elevator {elevator.Id}: Floor {elevator.CurrentFloor}, " +
$"Direction: {elevator.CurrentDirection}, State: {elevator.GetType()}, " +
$"Weight: {elevator.CurrentWeight.Kilograms}kg, " +
$"Pending Requests: {(elevator.HasPendingRequests() ? "Yes" : "No")}");
}
return sb.ToString();
}
}
}
7. Unit Tests
namespace ElevatorSystem.Tests
{
public class ElevatorStateTests
{
[Fact]
public async Task IdleState_OnRequest_ShouldStartMoving()
{
// Arrange
var elevator = new Elevator(1);
var request = Request.ExternalRequest(new Floor(5), Direction.Up);
// Act
elevator.AddRequest(request);
// Assert
await Task.Delay(100);
Assert.True(elevator.HasPendingRequests());
}
[Fact]
public async Task Elevator_MoveToFloor_ShouldArriveAtTarget()
{
// Arrange
var elevator = new Elevator(1);
var targetFloor = new Floor(5);
bool arrived = false;
elevator.FloorArrived += (s, e) =>
{
if (e.Floor.Number == targetFloor.Number)
arrived = true;
};
// Act
await elevator.MoveToFloorAsync(targetFloor);
// Assert
Assert.True(arrived);
Assert.Equal(targetFloor.Number, elevator.CurrentFloor.Number);
}
[Fact]
public void Request_ExternalRequest_ShouldCreateCorrectRequest()
{
// Arrange
var floor = new Floor(3);
// Act
var request = Request.ExternalRequest(floor, Direction.Up);
// Assert
Assert.Equal(floor, request.Floor);
Assert.Equal(Direction.Up, request.Direction);
Assert.Equal(RequestType.External, request.Type);
}
[Fact]
public void ElevatorDispatcher_ShouldSelectBestElevator()
{
// Arrange
var elevators = new List<Elevator>
{
new Elevator(1, 5), // At floor 5
new Elevator(2, 1), // At floor 1
new Elevator(3, 10) // At floor 10
};
var dispatcher = new ElevatorDispatcher(elevators);
var request = Request.ExternalRequest(new Floor(3), Direction.Down);
// Act
var selected = dispatcher.Dispatch(request);
// Assert
Assert.NotNull(selected);
// Elevator 2 at floor 1 is closest to floor 3 going down
Assert.Equal(2, selected.Id);
}
[Theory]
[InlineData(0, 1000, true)]
[InlineData(1500, 1000, false)]
public void Elevator_WeightCheck_ShouldDetectOverload(double currentWeight, double maxWeight, bool shouldBeNormal)
{
// Arrange
var elevator = new Elevator(1, maxWeight: maxWeight);
var overloadDetected = false;
elevator.AddWeight(currentWeight);
// Act & Assert
if (currentWeight > maxWeight)
{
Assert.Throws<InvalidOperationException>(() => elevator.ActivateEmergency());
}
}
}
public class DispatcherAlgorithmTests
{
[Fact]
public void Dispatch_SameDirection_ShouldPreferElevatorInPath()
{
// Arrange
var elevators = new List<Elevator>
{
new Elevator(1, 3) { Direction = Direction.Up },
new Elevator(2, 8) { Direction = Direction.Up }
};
var dispatcher = new ElevatorDispatcher(elevators);
var request = Request.ExternalRequest(new Floor(5), Direction.Up);
// Act
var selected = dispatcher.Dispatch(request);
// Assert
Assert.Equal(1, selected.Id); // Elevator 1 is in path
}
[Fact]
public void Dispatch_OppositeDirection_ShouldScoreLower()
{
// Arrange
var elevators = new List<Elevator>
{
new Elevator(1, 5) { Direction = Direction.Up },
new Elevator(2, 5) { Direction = Direction.Down }
};
var dispatcher = new ElevatorDispatcher(elevators);
var request = Request.ExternalRequest(new Floor(3), Direction.Down);
// Act
var selected = dispatcher.Dispatch(request);
// Assert
Assert.Equal(2, selected.Id); // Elevator 2 going same direction
}
}
}
8. Usage Example
public class Program
{
public static async Task Main(string[] args)
{
Console.WriteLine("=== Elevator System Simulation ===\n");
// Create control system with 10 floors and 3 elevators
var controlSystem = new ElevatorControlSystem(10, 3);
// Start the system
await controlSystem.StartAsync();
// Simulate random usage
Console.WriteLine("\n--- Simulating Random Requests ---");
await controlSystem.SimulateRandomUsageAsync(20, 2000);
// Simulate emergency
Console.WriteLine("\n--- Simulating Emergency ---");
await controlSystem.SimulateEmergencyAsync(1);
// Get final status
Console.WriteLine("\n--- Final System Status ---");
Console.WriteLine(controlSystem.GetSystemStatus());
// Stop the system
await controlSystem.StopAsync();
Console.WriteLine("\nSimulation completed. Press any key to exit.");
Console.ReadKey();
}
}
9. Summary
In this case study, we've built a complete Elevator System applying:
- State Pattern - Idle, Moving, DoorOpen, Overload, Emergency, Maintenance states
- Strategy Pattern - Different dispatching algorithms
- Observer Pattern - Floor panels listening to elevator events
- SOLID Principles - Single responsibility, open/closed, dependency inversion
- Event-Driven Design - Buttons, sensors, and display updates
- Concurrency Handling - Multiple elevators, request queuing
- Optimization Algorithms - Elevator dispatching based on distance and direction
📝 Practice Extensions:
- Add predictive scheduling - learn usage patterns to pre-position elevators
- Implement peak hour optimization algorithms
- Add energy-saving mode - reduce speed during low usage
- Implement voice control for accessibility
- Add destination dispatch system where users select floor before entering
- Implement real-time monitoring dashboard
- Add maintenance scheduling based on usage statistics
Happy Coding! 🚀