"Design the control system for a bank of elevators in a building. Support external hall calls (up/down buttons on each floor) and internal car calls (floor buttons inside each cabin), and decide which elevator services which request."
B. Difficulty
Intermediate → Advanced. The state machine for a single elevator is straightforward; the real difficulty is dispatch — deciding which of several elevators answers a hall call — and doing that assignment safely when multiple hall-call events can arrive concurrently.
C. What This Problem Tests
Modeling a single elevator as a State machine with real, enforceable transition rules (an elevator with doors open cannot start moving)
Separating "how one elevator behaves" from "which elevator answers this call" — the second is a Strategy, not a hardcoded rule buried in the elevator class
Recognizing the classic SCAN/"elevator algorithm" from OS disk scheduling as the direct ancestor of a realistic dispatch strategy
Atomic hall-call assignment under concurrent requests — the same check-then-act shape as Parking Lot's spot occupation, now applied to "which car claims this call"
D. Requirements
Functional
Support N elevators serving M floors in one building
Accept external hall calls (floor, direction) and internal car calls (destination floor from inside a cabin)
Assign each hall call to exactly one elevator
Move an elevator toward its pending requests, opening doors at each stop it must serve
Report each elevator's current floor, direction, and door state
Take an elevator out of service (maintenance/fault) and redistribute its pending requests
Non-Functional
No hall call may ever be silently dropped or assigned to two elevators simultaneously
An elevator already moving in a direction should not needlessly reverse before finishing the requests it can serve along the way (the core SCAN insight)
Door state transitions must be safe — a door cannot be commanded to open while the cabin is moving
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
How many elevators and floors, roughly?
A single elevator needs only a state machine; multiple elevators require a Dispatcher and an assignment strategy
Whether M (Dispatcher/Strategy) is in scope at all
2
Should the dispatch algorithm be swappable (e.g. nearest-car vs. zoning for peak hours)?
Determines whether dispatch logic is a Strategy interface or a single hardcoded method
Presence of DispatchStrategy
3
Is there a maximum weight/capacity per cabin?
Affects whether a full elevator can skip additional hall calls in its direction
Scope of Elevator.capacity check
4
Do any floors have restricted access (e.g. a penthouse requiring a keycard)?
Introduces an access-control concern layered on top of car calls
Excluded by assumption unless asked (see F)
5
Should the system optimize for anything beyond wait time (e.g. energy use, express floors)?
Determines whether the dispatch Strategy needs multiple swappable implementations or just one reasonable default
Scope of M / AB
F. Assumptions
Confirmed: N elevators, M floors, hall calls (floor + direction) and car calls (destination floor), single building.
Assumption: single process controlling all elevators (an in-building controller), not a multi-building fleet-management service — that class of problem is named explicitly but pushed to HLD (see Y).
Assumption: no keycard/restricted-floor access control unless asked — if asked, it layers on as a Proxy/guard check ahead of accepting a car call, the same shape as File System's optional permission Proxy.
Assumption: a reasonable default capacity limit exists per cabin, but weight-sensing hardware itself is out of scope — the design just needs a hook (isFull()) for the dispatcher to check.
G. Actors
Actor
Responsibility
Passenger
Issues hall calls (from a floor) and car calls (from inside a cabin)
ElevatorController (facade)
Top-level entry point; receives calls, delegates to Dispatcher
H. Core Domain Entities
Class
Responsibility
Key Attributes
Key Methods
Does NOT Handle
Elevator
Owns one cabin's physical state and its own pending-stop queues
State (headline): each Elevator holds a current ElevatorState and delegates behavior to it. A door can only be commanded open from DoorsOpenState-reachable transitions (i.e. after MovingState reports arrival) — illegal transitions like "open doors while moving" are structurally unrepresentable rather than guarded by scattered if checks.
Why Strategy for dispatch, not a hardcoded method: "which elevator answers this hall call" is exactly the kind of algorithm an interviewer expects to swap mid-round — nearest-car is the simple default, but zoning (dedicate elevators to floor bands during known traffic patterns) or destination-dispatch (passengers enter their destination floor at a hall kiosk, not just a direction) are common follow-ups. Isolating this behind DispatchStrategy.selectElevator() means the swap touches zero Elevator code.
Dispatcher as the atomicity boundary: the strategy only selects a candidate; the Dispatcher is what actually claims the call on that elevator under a lock, closing the same check-then-act gap this document has flagged in every resource-allocation problem so far (see W).
Why not model the Dispatcher itself as a Singleton: exactly one Dispatcher exists per building in practice, which might tempt a Singleton — but the controller already owns exactly one Dispatcher instance by composition, with no global static access needed anywhere else in the codebase. Reaching for Singleton here would repeat the naive shortcut this document has already flagged and rejected in Parking Lot and Ride-Sharing (see the Cross-Problem Synthesis Singleton contrast).
J. Class Diagram
View Mermaid source
classDiagram
class Elevator {
currentFloor
direction
doorState
+step()
+addStop(floor)
}
class ElevatorState {
<<interface>>
+handleArrival()
+canOpenDoor()
}
class DispatchStrategy {
<<interface>>
+selectElevator(call, elevators)
}
class Dispatcher {
+dispatch(hallCall)
}
class ElevatorController {
+requestElevator()
+selectFloor()
}
Elevator --> ElevatorState : current state
ElevatorState <|.. IdleState
ElevatorState <|.. MovingState
ElevatorState <|.. DoorsOpenState
ElevatorState <|.. OutOfServiceState
Dispatcher --> DispatchStrategy
DispatchStrategy <|.. NearestCarStrategy
DispatchStrategy <|.. ScanZoneStrategy
ElevatorController --> Dispatcher
Dispatcher --> "*" Elevator
K. Responsibility Mapping
Class
Responsibility
Does NOT Handle
Elevator
Own physical state, per-cabin stop queue, door mechanics
Deciding which call it should be assigned
ElevatorState implementations
One state's legal transitions and behavior
Anything about other elevators or dispatch
DispatchStrategy implementations
One algorithm for candidate elevator selection
Actually claiming/locking the assignment
Dispatcher
Atomic claim of a hall call on the strategy's chosen elevator; reassignment on failure
The selection algorithm itself
ElevatorController
Public API surface, wiring
Cabin mechanics, dispatch algorithm
L. SOLID Analysis
Principle
How It's Applied
Naive Violation
SRP
Elevator owns cabin mechanics; Dispatcher owns assignment; DispatchStrategy owns the algorithm
One Elevator class containing its own "find the best elevator among my siblings" method
OCP
A new dispatch algorithm (destination-dispatch) is a new DispatchStrategy implementation, zero changes elsewhere
A growing if (mode == ZONING) ... else if (mode == NEAREST) ... block inside Dispatcher
LSP
Every ElevatorState honestly answers canOpenDoor() for its own state rather than a generic default that's wrong for movement states
A shared base state that lets doors open by default unless explicitly overridden — one forgotten override becomes a safety bug
ISP
DispatchStrategy stays a single-method interface — nothing forces a strategy to also implement cabin-mechanic methods it has no business touching
A fat "ElevatorLogic" interface mixing dispatch selection with door timing constants
DIP
Dispatcher depends on the DispatchStrategy abstraction, not a concrete algorithm
Dispatcher.dispatch() calling a hardcoded nearestCar() method directly
M. Design Patterns
Pattern
Problem It Solves
Where It Appears
When NOT to Use
State
Elevator behavior and legal transitions differ sharply by mode, and illegal transitions are a real safety risk
Elevator / ElevatorState hierarchy
Skip only for a toy single-floor-shuttle with no door logic at all
Strategy
The dispatch algorithm is exactly the kind of thing an interviewer expects swapped mid-round
DispatchStrategy
Skip if the round explicitly fixes a single building with one elevator — then there's nothing to select among
Observer (optional)
Floor displays and in-cabin panels need to react to state changes without the Elevator knowing about UI concerns
Optional ElevatorObserver for displays
Skip if the round has no display/UI requirement
Explicitly avoided: Singleton for the Dispatcher — see I for why ordinary composition already gives exactly one instance without the global-access baggage Singleton would add.
N. What Is Likely to Change?
Behavior
Current Design
Anticipated Change
Protecting Abstraction
Dispatch algorithm
Nearest-car default
Zoning for peak traffic, destination-dispatch kiosks, ML-predicted demand
DispatchStrategy interface
Fleet size
Fixed N elevators known at startup
Elevators added/removed/serviced dynamically
Dispatcher holds a mutable collection, not a fixed array
Access control
None
Keycard-restricted floors
A guard check ahead of accepting a car call, same Proxy shape as File System's optional permission layer
O. Design Evolution
V1: single elevator, a boolean doorsOpen flag and inline movement logic, no dispatch at all.
V2: extract real ElevatorState classes once "doors should not open while moving" becomes an explicit safety requirement.
V3: add a second elevator, forcing a Dispatcher and a first DispatchStrategy (nearest-car).
V4: add a second strategy (zoning) once peak-traffic behavior is requested — validates the Strategy boundary drawn in V3.
V5: add out-of-service handling and pending-request redistribution.
V6: optional Observer layer for floor/cabin displays; optional access-control Proxy for restricted floors.
What stays stable:ElevatorController's public API. New dispatch algorithms are pure additions; fleet size changes don't touch Elevator or state classes at all.
P. API / Interface Design
interface ElevatorState {
void handleArrival(Elevator e);
boolean canOpenDoor();
}
interface DispatchStrategy {
Elevator selectElevator(HallCall call, List<Elevator> elevators);
}
class ElevatorController {
void requestElevator(int floor, Direction dir); // hall call
void selectFloor(String elevatorId, int floor); // car call
void setOutOfService(String elevatorId);
ElevatorStatus getStatus(String elevatorId);
}
interface ElevatorState {
void handleArrival(Elevator e);
boolean canOpenDoor();
}
interface DispatchStrategy {
Elevator selectElevator(HallCall call, List<Elevator> elevators);
}
class ElevatorController {
void requestElevator(int floor, Direction dir); // hall call
void selectFloor(String elevatorId, int floor); // car call
void setOutOfService(String elevatorId);
ElevatorStatus getStatus(String elevatorId);
}
class ElevatorState {
public:
virtual void handleArrival(std::shared_ptr<Elevator> e) = 0;
virtual bool canOpenDoor() = 0;
};
class DispatchStrategy {
public:
virtual std::shared_ptr<Elevator> selectElevator(const HallCall& call, const std::vector<std::shared_ptr<Elevator>>& elevators) = 0;
};
class ElevatorController {
public:
void requestElevator(int floor, Direction dir); // hall call
void selectFloor(const std::string& elevatorId, int floor); // car call
void setOutOfService(const std::string& elevatorId);
ElevatorStatus getStatus(const std::string& elevatorId);
};
class ElevatorState {
handleArrival(e) { throw new Error('not implemented'); }
canOpenDoor() { throw new Error('not implemented'); }
}
class DispatchStrategy {
selectElevator(call, elevators) { throw new Error('not implemented'); }
}
class ElevatorController {
requestElevator(floor, dir) {} // hall call
selectFloor(elevatorId, floor) {} // car call
setOutOfService(elevatorId) {}
getStatus(elevatorId) {}
}
Ownership/validation notes:requestElevator() constructs a HallCall and hands it to the Dispatcher, which selects a candidate via the active strategy and then atomically claims that call on the candidate's stop queue — if the claim fails (e.g. the candidate went out of service in the interim), it retries selection rather than dropping the call.
Q. Pseudocode — Critical Logic
Dispatcher.dispatch(hallCall):
loop:
candidate = strategy.selectElevator(hallCall, activeElevators)
if candidate is null: queue call for retry; return
lock(candidate)
try:
if candidate.isInService() and not candidate.isFull():
candidate.addStop(hallCall.floor)
return // claimed
finally:
unlock(candidate)
// candidate became unavailable between selection and claim — retry with remaining elevators
NearestCarStrategy.selectElevator(call, elevators):
best = null; bestDistance = infinity
for e in elevators:
if not e.isInService(): continue
if e.direction == call.direction or e.direction == IDLE:
d = abs(e.currentFloor - call.floor)
if d < bestDistance: best = e; bestDistance = d
return best
void dispatch(HallCall hallCall) {
while (true) {
Elevator candidate = strategy.selectElevator(hallCall, activeElevators);
if (candidate == null) { queueForRetry(hallCall); return; }
lock(candidate);
try {
if (candidate.isInService() && !candidate.isFull()) {
candidate.addStop(hallCall.getFloor());
return; // claimed
}
} finally {
unlock(candidate);
}
// candidate became unavailable between selection and claim — retry with remaining elevators
}
}
Elevator selectElevator(HallCall call, List<Elevator> elevators) {
Elevator best = null;
double bestDistance = Double.POSITIVE_INFINITY;
for (Elevator e : elevators) {
if (!e.isInService()) continue;
if (e.getDirection() == call.getDirection() || e.getDirection() == Direction.IDLE) {
double d = Math.abs(e.getCurrentFloor() - call.getFloor());
if (d < bestDistance) { best = e; bestDistance = d; }
}
}
return best;
}
void Dispatcher::dispatch(const HallCall& hallCall) {
while (true) {
auto candidate = strategy->selectElevator(hallCall, activeElevators);
if (candidate == nullptr) { queueForRetry(hallCall); return; }
lock(candidate);
if (candidate->isInService() && !candidate->isFull()) {
candidate->addStop(hallCall.floor);
unlock(candidate);
return; // claimed
}
unlock(candidate);
// candidate became unavailable between selection and claim — retry with remaining elevators
}
}
std::shared_ptr<Elevator> NearestCarStrategy::selectElevator(const HallCall& call, const std::vector<std::shared_ptr<Elevator>>& elevators) {
std::shared_ptr<Elevator> best = nullptr;
double bestDistance = std::numeric_limits<double>::infinity();
for (auto& e : elevators) {
if (!e->isInService()) continue;
if (e->direction == call.direction || e->direction == Direction::IDLE) {
double d = std::abs(e->currentFloor - call.floor);
if (d < bestDistance) { best = e; bestDistance = d; }
}
}
return best;
}
dispatch(hallCall) {
while (true) {
const candidate = this.strategy.selectElevator(hallCall, this.activeElevators);
if (candidate === null) { this.queueForRetry(hallCall); return; }
lock(candidate);
try {
if (candidate.isInService() && !candidate.isFull()) {
candidate.addStop(hallCall.floor);
return; // claimed
}
} finally {
unlock(candidate);
}
// candidate became unavailable between selection and claim — retry with remaining elevators
}
}
selectElevator(call, elevators) {
let best = null;
let bestDistance = Infinity;
for (const e of elevators) {
if (!e.isInService()) continue;
if (e.direction === call.direction || e.direction === Direction.IDLE) {
const d = Math.abs(e.currentFloor - call.floor);
if (d < bestDistance) { best = e; bestDistance = d; }
}
}
return best;
}
dispatch(hallCall: HallCall): void {
while (true) {
const candidate = this.strategy.selectElevator(hallCall, this.activeElevators);
if (candidate === null) { this.queueForRetry(hallCall); return; }
lock(candidate);
try {
if (candidate.isInService() && !candidate.isFull()) {
candidate.addStop(hallCall.floor);
return; // claimed
}
} finally {
unlock(candidate);
}
// candidate became unavailable between selection and claim — retry with remaining elevators
}
}
selectElevator(call: HallCall, elevators: Elevator[]): Elevator | null {
let best: Elevator | null = null;
let bestDistance = Infinity;
for (const e of elevators) {
if (!e.isInService()) continue;
if (e.direction === call.direction || e.direction === Direction.IDLE) {
const d = Math.abs(e.currentFloor - call.floor);
if (d < bestDistance) { best = e; bestDistance = d; }
}
}
return best;
}
R. End-to-End Use Case
Passenger on floor 3 presses "Up"
│
▼
requestElevator(3, UP)
│ Dispatcher asks NearestCarStrategy for a candidate among in-service elevators
│ Elevator B (idle, floor 1) selected over Elevator A (moving down, floor 8)
│ Dispatcher locks B, confirms still in-service, adds stop(3)
▼
Elevator B: IdleState → MovingState → arrives floor 3 → DoorsOpenState
│ Passenger enters, presses floor 7 (car call)
▼
selectFloor("B", 7) → addStop(7) → doors close → MovingState → arrives 7 → DoorsOpenState
S. Data Structures
Structure
Used For
Why
Alternative & Trade-off
Two priority queues per elevator (up-stops min-heap, down-stops max-heap)
Ordering pending stops along the current direction of travel
Direct analog of the SCAN/"elevator algorithm" from OS disk scheduling — services all stops in the current direction before reversing, minimizing needless direction changes
A single FIFO queue is simpler but reverses direction far more often than necessary, hurting average wait time
Map<String, Elevator> in Dispatcher
Fleet lookup by id
O(1) access for status queries and out-of-service toggling
A plain list also works at small N; the map only matters once lookups by id are frequent
T. Complexity Analysis
Operation
Time
Space
selectElevator() (nearest-car)
O(N) elevators scanned
O(1)
addStop() / next-stop lookup
O(log k) heap operations, k = pending stops for that elevator
O(k)
U. Testing Strategy
Happy path: a hall call is answered by exactly one elevator; car calls after boarding are served in SCAN order, not arrival order.
Edge cases: a full cabin skips further hall calls in its direction until it drops below capacity; an out-of-service elevator is never selected and its pending stops are redistributed.
Safety invariant:canOpenDoor() is false for every state except a stopped arrival — attempting to open doors mid-MovingState is rejected at the state level, not by a caller-side check.
Concurrency tests: many simultaneous hall calls across floors never result in the same call claimed by two elevators, nor silently dropped, under high contention.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Pattern/Trade-off
"Add zoning: dedicate elevators 1-3 to floors 1-10 during morning peak."
Recognizing this as a new Strategy, not a special case in Dispatcher
New ScanZoneStrategy, swapped in by time-of-day
New class only
Validates the Strategy investment from M
"An elevator breaks down mid-transit with passengers inside."
State-machine completeness under faults
OutOfServiceState reachable from any state; pending stops redistributed by Dispatcher
Elevator, Dispatcher
Directly exercises Z
"Scale this to a 40-building campus with a shared dispatch brain."
LLD/HLD boundary awareness
N/A at the LLD layer — candidate should name a fleet-management service and cross-building routing as HLD concerns
N/A
Recognizing the boundary, not designing it live
W. Concurrency
Shared mutable state: each elevator's stop queues and in-service flag; the Dispatcher's view of which elevators are available.
The check-then-act race, once more: selecting a candidate (read) and claiming the call on it (write) are two separate steps; if not locked around the claim, two concurrent hall calls could both select the same idle elevator before either claims it, or a call could be assigned to an elevator that goes out-of-service in between. The Dispatcher closes this the same way Parking Lot closes spot-occupation: lock the candidate, re-verify eligibility, then commit — not select-then-blindly-assign.
Per-elevator locking, not a global dispatch lock: claiming a call only needs to lock the one candidate elevator, so unrelated elevators keep processing their own stops without contention.
X. Persistence
Elevator and fleet state is operational, not something that needs durable storage across restarts in most rounds — a restart can reasonably reinitialize all cabins to floor 1, idle. If asked, only aggregate metrics (trip counts, average wait time) would be persisted, behind a simple repository interface, without touching Elevator or Dispatcher.
Y. Scaling Boundary
Layer
What Belongs Here
LLD
Per-elevator state machine, dispatch strategy, atomic call claiming, out-of-service redistribution
Cross-building routing and predictive dispatch — named but not designed unless the interviewer pivots there
Z. Failure Scenarios
Elevator stalls between floors: transitions to OutOfServiceState from any prior state; the Dispatcher immediately redistributes its unclaimed stops to other in-service elevators via the same strategy path used for fresh calls.
Door obstruction mid-close: a sensor-triggered re-open is just another legal transition from the closing sub-state back to DoorsOpenState — handled entirely within the state machine, no Dispatcher involvement.
Power loss mid-transit: out of scope for pure LLD (this is a hardware/failsafe-brake concern), but the design should not assume it can't happen — OutOfServiceState being reachable from MovingState is exactly what makes recovery representable at all.
AA. Common Bad Designs
A single Elevator class that also decides which of its siblings should answer a call — couples cabin mechanics to fleet-wide decisions and makes every new dispatch algorithm a change to every elevator instance.
FIFO stop queue instead of direction-ordered (SCAN) queues — technically correct but reverses direction far more often than necessary, the elevator equivalent of naive disk scheduling.
Selecting a candidate and assigning the call as one non-atomic step — passes single-threaded tests, double-assigns or drops calls under real concurrency.
Singleton Dispatcher "because there's only one" — see I; ordinary composition already provides exactly one instance without the global-access downsides.
AB. Design Trade-Offs
Decision
Option A
Option B
Recommendation
Dispatch algorithm
Nearest-car (minimize this call's wait)
Zoning (minimize aggregate wait under known traffic patterns)
Nearest-car as the honest simple default; name zoning explicitly as the peak-traffic follow-up rather than over-building it unprompted
Full-cabin behavior
Skip further hall calls in that direction
Stop anyway and let the panel refuse new car calls
Skip — stopping for a call the cabin can't actually serve wastes everyone's time including the waiting passenger's
AC. Interviewer Evaluation
Strong signal: proposing State for the elevator's own behavior unprompted; separating dispatch algorithm from dispatch mechanism (Strategy vs. Dispatcher) without being led there; naming the SCAN/disk-scheduling analogy; catching the select-then-claim race before being walked into it.
Red flag: one Elevator god-class handling both cabin mechanics and fleet-wide selection; a FIFO stop queue presented as the final answer; treating "which elevator answers this" as a static rule instead of a swappable Strategy.
AD. Interview Simulation
Interviewer: Design a bank of elevators.
Candidate: Each elevator's own behavior is a State machine — idle, moving, doors-open, out-of-service — because the illegal transitions matter for safety, like never opening doors while moving.
Interviewer: Now there are five elevators. Who decides which one answers a given floor's call?
Candidate: That's a separate concern from the elevator itself — a Dispatcher using a swappable DispatchStrategy. A simple default picks the nearest elevator already heading the right direction or idle; if you need zoning for peak hours later, that's a new strategy, not a rewrite of the elevator classes.
Interviewer: What happens if two hall calls come in at the same instant and both would naturally go to the same idle elevator?
Candidate: Selecting the candidate and actually claiming the call have to be separate steps, and the claim has to be locked and re-verified — otherwise you get the same race as two people grabbing the last parking spot. Whichever claim commits first wins; the other retries selection against the now-updated fleet state.
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
/**
* Elevator System — full class-diagram implementation.
*
* Elevator delegates behavior to its current ElevatorState (State pattern) so illegal
* transitions (e.g. opening doors while moving) are structurally unrepresentable.
* Dispatcher uses a swappable DispatchStrategy to select a candidate elevator, then
* atomically claims the hall call under a per-elevator lock (closing the check-then-act
* gap between "selected" and "claimed").
*/
public class p11 {
enum Direction { UP, DOWN, IDLE }
enum DoorState { OPEN, CLOSED }
// ---------- Value objects ----------
static final class HallCall {
final int floor;
final Direction direction;
HallCall(int floor, Direction direction) { this.floor = floor; this.direction = direction; }
}
static final class CarCall {
final int destinationFloor;
CarCall(int destinationFloor) { this.destinationFloor = destinationFloor; }
}
// ---------- ElevatorState (interface, State pattern) ----------
interface ElevatorState {
void handleArrival(Elevator e);
boolean canOpenDoor();
String name();
}
static final class IdleState implements ElevatorState {
@Override public void handleArrival(Elevator e) { /* nothing pending; stays idle */ }
@Override public boolean canOpenDoor() { return true; }
@Override public String name() { return "IDLE"; }
}
static final class MovingState implements ElevatorState {
@Override public void handleArrival(Elevator e) {
// The cabin reports arrival at a floor it must serve -> transition to DoorsOpenState.
e.setState(new DoorsOpenState());
e.openDoor();
}
@Override public boolean canOpenDoor() { return false; } // cannot open while moving
@Override public String name() { return "MOVING"; }
}
static final class DoorsOpenState implements ElevatorState {
@Override public void handleArrival(Elevator e) { /* already stopped */ }
@Override public boolean canOpenDoor() { return true; }
@Override public String name() { return "DOORS_OPEN"; }
}
static final class OutOfServiceState implements ElevatorState {
@Override public void handleArrival(Elevator e) { /* no-op: not serving calls */ }
@Override public boolean canOpenDoor() { return false; }
@Override public String name() { return "OUT_OF_SERVICE"; }
}
// ---------- Elevator ----------
/** Owns one cabin's physical state and its own pending-stop queue. */
static final class Elevator {
final String id;
final int capacity;
volatile int currentFloor = 0;
volatile Direction direction = Direction.IDLE;
volatile DoorState doorState = DoorState.CLOSED;
private ElevatorState state = new IdleState();
private final TreeSet<Integer> upStops = new TreeSet<>();
private final TreeSet<Integer> downStops = new TreeSet<>();
final ReentrantLock lock = new ReentrantLock();
Elevator(String id, int capacity) { this.id = id; this.capacity = capacity; }
void setState(ElevatorState state) { this.state = state; }
ElevatorState getState() { return state; }
boolean isInService() { return !(state instanceof OutOfServiceState); }
boolean isFull() { return false; } // capacity tracking simplified for this demo
/** Add a stop request; SCAN-style: keep the elevator moving in its current direction if possible. */
void addStop(int floor) {
if (floor > currentFloor) upStops.add(floor);
else if (floor < currentFloor) downStops.add(floor);
else { /* already there: open doors immediately */ }
if (direction == Direction.IDLE && (!upStops.isEmpty() || !downStops.isEmpty())) {
direction = !upStops.isEmpty() ? Direction.UP : Direction.DOWN;
setState(new MovingState());
}
}
/** Advance the cabin by one floor toward its next pending stop (SCAN algorithm). */
void step() {
if (!(state instanceof MovingState) && !(state instanceof IdleState)) return;
if (direction == Direction.UP) {
if (upStops.isEmpty()) {
direction = downStops.isEmpty() ? Direction.IDLE : Direction.DOWN;
if (direction == Direction.IDLE) setState(new IdleState());
return;
}
currentFloor++;
if (upStops.contains(currentFloor)) {
upStops.remove(currentFloor);
state.handleArrival(this);
}
} else if (direction == Direction.DOWN) {
if (downStops.isEmpty()) {
direction = upStops.isEmpty() ? Direction.IDLE : Direction.UP;
if (direction == Direction.IDLE) setState(new IdleState());
return;
}
currentFloor--;
if (downStops.contains(currentFloor)) {
downStops.remove(currentFloor);
state.handleArrival(this);
}
}
}
void openDoor() {
if (!state.canOpenDoor()) throw new IllegalStateException("Cannot open doors in state " + state.name());
doorState = DoorState.OPEN;
}
void closeDoor() {
doorState = DoorState.CLOSED;
// Resume motion after servicing this floor, or go idle if nothing left.
if (!upStops.isEmpty() || !downStops.isEmpty()) setState(new MovingState());
else { direction = Direction.IDLE; setState(new IdleState()); }
}
void setOutOfService() { setState(new OutOfServiceState()); direction = Direction.IDLE; }
List<Integer> pendingStops() {
List<Integer> all = new ArrayList<>(upStops);
all.addAll(downStops);
return all;
}
}
// ---------- DispatchStrategy (interface, Strategy pattern) ----------
interface DispatchStrategy {
Elevator selectElevator(HallCall call, List<Elevator> elevators);
}
static final class NearestCarStrategy implements DispatchStrategy {
@Override
public Elevator selectElevator(HallCall call, List<Elevator> elevators) {
Elevator best = null;
int bestDistance = Integer.MAX_VALUE;
for (Elevator e : elevators) {
if (!e.isInService()) continue;
if (e.direction == call.direction || e.direction == Direction.IDLE) {
int d = Math.abs(e.currentFloor - call.floor);
if (d < bestDistance) { best = e; bestDistance = d; }
}
}
return best;
}
}
/** Zones the building into bands; prefers an elevator already assigned near that band. */
static final class ScanZoneStrategy implements DispatchStrategy {
private final int zoneSize;
ScanZoneStrategy(int zoneSize) { this.zoneSize = zoneSize; }
@Override
public Elevator selectElevator(HallCall call, List<Elevator> elevators) {
int callZone = call.floor / zoneSize;
Elevator best = null;
int bestScore = Integer.MAX_VALUE;
for (Elevator e : elevators) {
if (!e.isInService()) continue;
int elevatorZone = e.currentFloor / zoneSize;
int zoneDistance = Math.abs(elevatorZone - callZone) * 1000;
int floorDistance = Math.abs(e.currentFloor - call.floor);
int score = zoneDistance + floorDistance;
if (score < bestScore) { best = e; bestScore = score; }
}
return best;
}
}
// ---------- Dispatcher ----------
/** Atomically assigns each incoming hall call to one elevator via the active strategy. */
static final class Dispatcher {
private final List<Elevator> elevators;
private volatile DispatchStrategy strategy;
Dispatcher(List<Elevator> elevators, DispatchStrategy strategy) {
this.elevators = elevators;
this.strategy = strategy;
}
void setStrategy(DispatchStrategy strategy) { this.strategy = strategy; }
/** Select then atomically claim; retries against remaining elevators if claim fails. */
void dispatch(HallCall call) {
List<Elevator> remaining = new ArrayList<>(elevators);
while (!remaining.isEmpty()) {
Elevator candidate = strategy.selectElevator(call, remaining);
if (candidate == null) return; // no elevator can currently serve this call
candidate.lock.lock();
try {
if (candidate.isInService() && !candidate.isFull()) {
candidate.addStop(call.floor);
return; // claimed
}
} finally {
candidate.lock.unlock();
}
// candidate became unavailable between selection and claim -- retry with the rest
remaining.remove(candidate);
}
}
}
// ---------- ElevatorController (facade) ----------
static final class ElevatorStatus {
final String id; final int currentFloor; final Direction direction; final DoorState doorState; final String state;
ElevatorStatus(Elevator e) {
this.id = e.id; this.currentFloor = e.currentFloor; this.direction = e.direction;
this.doorState = e.doorState; this.state = e.getState().name();
}
@Override public String toString() {
return String.format("Elevator[%s] floor=%d dir=%s door=%s state=%s", id, currentFloor, direction, doorState, state);
}
}
static final class ElevatorController {
private final Dispatcher dispatcher;
private final Map<String, Elevator> elevators = new LinkedHashMap<>();
ElevatorController(List<Elevator> elevatorList, DispatchStrategy strategy) {
for (Elevator e : elevatorList) elevators.put(e.id, e);
this.dispatcher = new Dispatcher(elevatorList, strategy);
}
void requestElevator(int floor, Direction dir) {
dispatcher.dispatch(new HallCall(floor, dir));
}
void selectFloor(String elevatorId, int floor) {
Elevator e = elevators.get(elevatorId);
if (e == null) throw new IllegalArgumentException("No such elevator: " + elevatorId);
e.lock.lock();
try {
e.addStop(floor); // car call: destination chosen from inside the cabin
} finally {
e.lock.unlock();
}
}
void setOutOfService(String elevatorId) {
Elevator e = elevators.get(elevatorId);
if (e == null) return;
e.lock.lock();
try {
List<Integer> pending = e.pendingStops();
e.setOutOfService();
// Redistribute this elevator's pending requests to the rest of the fleet.
for (int floor : pending) {
dispatcher.dispatch(new HallCall(floor, Direction.IDLE));
}
} finally {
e.lock.unlock();
}
}
ElevatorStatus getStatus(String elevatorId) {
Elevator e = elevators.get(elevatorId);
if (e == null) throw new IllegalArgumentException("No such elevator: " + elevatorId);
return new ElevatorStatus(e);
}
/**
* Advance every in-service elevator by one simulated time-step. An elevator that
* just opened its doors (previous tick's arrival) spends this tick "boarding" and
* closes its doors, resuming motion on the following tick.
*/
void tick() {
for (Elevator e : elevators.values()) {
e.lock.lock();
try {
if (e.getState() instanceof DoorsOpenState) e.closeDoor();
else e.step();
} finally {
e.lock.unlock();
}
}
}
}
// ---------- Demo ----------
public static void main(String[] args) {
List<Elevator> cabins = List.of(new Elevator("E1", 8), new Elevator("E2", 8));
ElevatorController controller = new ElevatorController(cabins, new NearestCarStrategy());
controller.requestElevator(5, Direction.UP);
controller.requestElevator(2, Direction.DOWN);
controller.selectFloor("E1", 8);
for (int i = 0; i < 12; i++) {
controller.tick();
}
System.out.println(controller.getStatus("E1"));
System.out.println(controller.getStatus("E2"));
controller.setOutOfService("E2");
System.out.println("After maintenance: " + controller.getStatus("E2"));
}
}
// Elevator System -- full class-diagram implementation.
//
// Elevator delegates behavior to its current ElevatorState (State pattern) so illegal
// transitions (e.g. opening doors while moving) are structurally unrepresentable.
// Dispatcher uses a swappable DispatchStrategy to select a candidate elevator, then
// atomically claims the hall call under a per-elevator lock.
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <memory>
#include <mutex>
#include <algorithm>
#include <stdexcept>
#include <cstdlib>
enum class Direction { UP, DOWN, IDLE };
enum class DoorState { OPEN, CLOSED };
std::string toString(Direction d) {
switch (d) { case Direction::UP: return "UP"; case Direction::DOWN: return "DOWN"; default: return "IDLE"; }
}
std::string toString(DoorState d) { return d == DoorState::OPEN ? "OPEN" : "CLOSED"; }
// ---------- Value objects ----------
struct HallCall { int floor; Direction direction; };
struct CarCall { int destinationFloor; };
class Elevator; // fwd decl
// ---------- ElevatorState (interface, State pattern) ----------
struct ElevatorState {
virtual ~ElevatorState() = default;
virtual void handleArrival(Elevator& e) = 0;
virtual bool canOpenDoor() const = 0;
virtual std::string name() const = 0;
};
// ---------- Elevator ----------
// Owns one cabin's physical state and its own pending-stop queue.
class Elevator {
public:
std::string id;
int capacity;
int currentFloor = 0;
Direction direction = Direction::IDLE;
DoorState doorState = DoorState::CLOSED;
std::mutex lock;
Elevator(std::string id_, int capacity_) : id(std::move(id_)), capacity(capacity_) {
state_ = makeIdle();
}
ElevatorState* getState() { return state_.get(); }
void setState(std::unique_ptr<ElevatorState> s) { state_ = std::move(s); }
bool isInService() const { return stateName_ != "OUT_OF_SERVICE"; }
bool isFull() const { return false; } // capacity tracking simplified for this demo
// Add a stop request; SCAN-style: keep the elevator moving in its current direction if possible.
void addStop(int floor) {
if (floor > currentFloor) upStops_.insert(floor);
else if (floor < currentFloor) downStops_.insert(floor);
if (direction == Direction::IDLE && (!upStops_.empty() || !downStops_.empty())) {
direction = !upStops_.empty() ? Direction::UP : Direction::DOWN;
setMoving();
}
}
// Advance the cabin by one floor toward its next pending stop (SCAN algorithm).
void step();
void openDoor() {
if (!state_->canOpenDoor()) throw std::runtime_error("Cannot open doors in state " + state_->name());
doorState = DoorState::OPEN;
}
void closeDoor() {
doorState = DoorState::CLOSED;
if (!upStops_.empty() || !downStops_.empty()) setMoving();
else { direction = Direction::IDLE; setIdle(); }
}
void setOutOfService();
std::vector<int> pendingStops() const {
std::vector<int> all(upStops_.begin(), upStops_.end());
all.insert(all.end(), downStops_.begin(), downStops_.end());
return all;
}
void setIdle();
void setMoving();
void setDoorsOpen();
private:
std::unique_ptr<ElevatorState> state_;
std::string stateName_ = "IDLE";
std::set<int> upStops_;
std::set<int> downStops_;
std::unique_ptr<ElevatorState> makeIdle();
std::unique_ptr<ElevatorState> makeMoving();
std::unique_ptr<ElevatorState> makeDoorsOpen();
std::unique_ptr<ElevatorState> makeOutOfService();
friend struct IdleState;
friend struct MovingState;
friend struct DoorsOpenState;
friend struct OutOfServiceState;
};
struct IdleState : ElevatorState {
void handleArrival(Elevator&) override {}
bool canOpenDoor() const override { return true; }
std::string name() const override { return "IDLE"; }
};
struct MovingState : ElevatorState {
void handleArrival(Elevator& e) override {
// The cabin reports arrival at a floor it must serve -> transition to DoorsOpenState.
e.setDoorsOpen();
e.openDoor();
}
bool canOpenDoor() const override { return false; } // cannot open while moving
std::string name() const override { return "MOVING"; }
};
struct DoorsOpenState : ElevatorState {
void handleArrival(Elevator&) override {}
bool canOpenDoor() const override { return true; }
std::string name() const override { return "DOORS_OPEN"; }
};
struct OutOfServiceState : ElevatorState {
void handleArrival(Elevator&) override {}
bool canOpenDoor() const override { return false; }
std::string name() const override { return "OUT_OF_SERVICE"; }
};
std::unique_ptr<ElevatorState> Elevator::makeIdle() { return std::make_unique<IdleState>(); }
std::unique_ptr<ElevatorState> Elevator::makeMoving() { return std::make_unique<MovingState>(); }
std::unique_ptr<ElevatorState> Elevator::makeDoorsOpen() { return std::make_unique<DoorsOpenState>(); }
std::unique_ptr<ElevatorState> Elevator::makeOutOfService() { return std::make_unique<OutOfServiceState>(); }
void Elevator::setIdle() { state_ = makeIdle(); stateName_ = "IDLE"; }
void Elevator::setMoving() { state_ = makeMoving(); stateName_ = "MOVING"; }
void Elevator::setDoorsOpen() { state_ = makeDoorsOpen(); stateName_ = "DOORS_OPEN"; }
void Elevator::setOutOfService() { state_ = makeOutOfService(); stateName_ = "OUT_OF_SERVICE"; direction = Direction::IDLE; }
void Elevator::step() {
if (stateName_ != "MOVING" && stateName_ != "IDLE") return;
if (direction == Direction::UP) {
if (upStops_.empty()) {
direction = downStops_.empty() ? Direction::IDLE : Direction::DOWN;
if (direction == Direction::IDLE) setIdle();
return;
}
currentFloor++;
if (upStops_.count(currentFloor)) {
upStops_.erase(currentFloor);
state_->handleArrival(*this);
}
} else if (direction == Direction::DOWN) {
if (downStops_.empty()) {
direction = upStops_.empty() ? Direction::IDLE : Direction::UP;
if (direction == Direction::IDLE) setIdle();
return;
}
currentFloor--;
if (downStops_.count(currentFloor)) {
downStops_.erase(currentFloor);
state_->handleArrival(*this);
}
}
}
// ---------- DispatchStrategy (interface, Strategy pattern) ----------
struct DispatchStrategy {
virtual ~DispatchStrategy() = default;
virtual Elevator* selectElevator(const HallCall& call, const std::vector<Elevator*>& elevators) = 0;
};
struct NearestCarStrategy : DispatchStrategy {
Elevator* selectElevator(const HallCall& call, const std::vector<Elevator*>& elevators) override {
Elevator* best = nullptr;
int bestDistance = INT32_MAX;
for (auto* e : elevators) {
if (!e->isInService()) continue;
if (e->direction == call.direction || e->direction == Direction::IDLE) {
int d = std::abs(e->currentFloor - call.floor);
if (d < bestDistance) { best = e; bestDistance = d; }
}
}
return best;
}
};
// Zones the building into bands; prefers an elevator already assigned near that band.
struct ScanZoneStrategy : DispatchStrategy {
int zoneSize;
explicit ScanZoneStrategy(int zoneSize_) : zoneSize(zoneSize_) {}
Elevator* selectElevator(const HallCall& call, const std::vector<Elevator*>& elevators) override {
int callZone = call.floor / zoneSize;
Elevator* best = nullptr;
int bestScore = INT32_MAX;
for (auto* e : elevators) {
if (!e->isInService()) continue;
int elevatorZone = e->currentFloor / zoneSize;
int score = std::abs(elevatorZone - callZone) * 1000 + std::abs(e->currentFloor - call.floor);
if (score < bestScore) { best = e; bestScore = score; }
}
return best;
}
};
// ---------- Dispatcher ----------
// Atomically assigns each incoming hall call to one elevator via the active strategy.
class Dispatcher {
public:
Dispatcher(std::vector<Elevator*> elevators, std::shared_ptr<DispatchStrategy> strategy)
: elevators_(std::move(elevators)), strategy_(std::move(strategy)) {}
void setStrategy(std::shared_ptr<DispatchStrategy> strategy) { strategy_ = std::move(strategy); }
// Select then atomically claim; retries against remaining elevators if claim fails.
void dispatch(const HallCall& call) {
std::vector<Elevator*> remaining = elevators_;
while (!remaining.empty()) {
Elevator* candidate = strategy_->selectElevator(call, remaining);
if (candidate == nullptr) return; // no elevator can currently serve this call
{
std::lock_guard<std::mutex> guard(candidate->lock);
if (candidate->isInService() && !candidate->isFull()) {
candidate->addStop(call.floor);
return; // claimed
}
}
remaining.erase(std::remove(remaining.begin(), remaining.end(), candidate), remaining.end());
}
}
private:
std::vector<Elevator*> elevators_;
std::shared_ptr<DispatchStrategy> strategy_;
};
// ---------- ElevatorController (facade) ----------
struct ElevatorStatus {
std::string id; int currentFloor; Direction direction; DoorState doorState; std::string state;
};
std::ostream& operator<<(std::ostream& os, const ElevatorStatus& s) {
os << "Elevator[" << s.id << "] floor=" << s.currentFloor << " dir=" << toString(s.direction)
<< " door=" << toString(s.doorState) << " state=" << s.state;
return os;
}
class ElevatorController {
public:
ElevatorController(std::vector<std::unique_ptr<Elevator>> elevatorList, std::shared_ptr<DispatchStrategy> strategy) {
elevators_ = std::move(elevatorList);
std::vector<Elevator*> raw;
for (auto& e : elevators_) raw.push_back(e.get());
dispatcher_ = std::make_unique<Dispatcher>(raw, std::move(strategy));
}
void requestElevator(int floor, Direction dir) {
dispatcher_->dispatch(HallCall{floor, dir});
}
void selectFloor(const std::string& elevatorId, int floor) {
Elevator* e = find(elevatorId);
std::lock_guard<std::mutex> guard(e->lock);
e->addStop(floor); // car call: destination chosen from inside the cabin
}
void setOutOfService(const std::string& elevatorId) {
Elevator* e = find(elevatorId);
std::vector<int> pending;
{
std::lock_guard<std::mutex> guard(e->lock);
pending = e->pendingStops();
e->setOutOfService();
}
// Redistribute this elevator's pending requests to the rest of the fleet.
for (int floor : pending) dispatcher_->dispatch(HallCall{floor, Direction::IDLE});
}
ElevatorStatus getStatus(const std::string& elevatorId) {
Elevator* e = find(elevatorId);
return ElevatorStatus{e->id, e->currentFloor, e->direction, e->doorState, e->getState()->name()};
}
// Advance every in-service elevator by one simulated time-step; an elevator that
// just opened its doors spends this tick "boarding" and closes them, resuming next tick.
void tick() {
for (auto& e : elevators_) {
std::lock_guard<std::mutex> guard(e->lock);
if (e->getState()->name() == "DOORS_OPEN") e->closeDoor();
else e->step();
}
}
private:
std::vector<std::unique_ptr<Elevator>> elevators_;
std::unique_ptr<Dispatcher> dispatcher_;
Elevator* find(const std::string& id) {
for (auto& e : elevators_) if (e->id == id) return e.get();
throw std::invalid_argument("No such elevator: " + id);
}
};
int main() {
std::vector<std::unique_ptr<Elevator>> cabins;
cabins.push_back(std::make_unique<Elevator>("E1", 8));
cabins.push_back(std::make_unique<Elevator>("E2", 8));
ElevatorController controller(std::move(cabins), std::make_shared<NearestCarStrategy>());
controller.requestElevator(5, Direction::UP);
controller.requestElevator(2, Direction::DOWN);
controller.selectFloor("E1", 8);
for (int i = 0; i < 12; i++) controller.tick();
std::cout << controller.getStatus("E1") << "\n";
std::cout << controller.getStatus("E2") << "\n";
controller.setOutOfService("E2");
std::cout << "After maintenance: " << controller.getStatus("E2") << "\n";
return 0;
}
'use strict';
/**
* Elevator System -- full class-diagram implementation.
*
* Elevator delegates behavior to its current ElevatorState (State pattern) so illegal
* transitions (e.g. opening doors while moving) are structurally unrepresentable.
* Dispatcher uses a swappable DispatchStrategy to select a candidate elevator, then
* atomically claims the hall call under a per-elevator "lock" (a synchronous critical
* section -- Node has no preemptive threading, but the same check-then-act gap this
* models is real once you add async I/O around dispatch, so we still guard it explicitly).
*/
const Direction = { UP: 'UP', DOWN: 'DOWN', IDLE: 'IDLE' };
const DoorState = { OPEN: 'OPEN', CLOSED: 'CLOSED' };
// ---------- ElevatorState (interface, State pattern) ----------
class ElevatorState {
handleArrival(_elevator) { throw new Error('not implemented'); }
canOpenDoor() { throw new Error('not implemented'); }
name() { throw new Error('not implemented'); }
}
class IdleState extends ElevatorState {
handleArrival(_e) {}
canOpenDoor() { return true; }
name() { return 'IDLE'; }
}
class MovingState extends ElevatorState {
handleArrival(e) {
// The cabin reports arrival at a floor it must serve -> transition to DoorsOpenState.
e.setState(new DoorsOpenState());
e.openDoor();
}
canOpenDoor() { return false; } // cannot open while moving
name() { return 'MOVING'; }
}
class DoorsOpenState extends ElevatorState {
handleArrival(_e) {}
canOpenDoor() { return true; }
name() { return 'DOORS_OPEN'; }
}
class OutOfServiceState extends ElevatorState {
handleArrival(_e) {}
canOpenDoor() { return false; }
name() { return 'OUT_OF_SERVICE'; }
}
// ---------- Elevator ----------
// Owns one cabin's physical state and its own pending-stop queue.
class Elevator {
constructor(id, capacity) {
this.id = id;
this.capacity = capacity;
this.currentFloor = 0;
this.direction = Direction.IDLE;
this.doorState = DoorState.CLOSED;
this._state = new IdleState();
this._upStops = new Set();
this._downStops = new Set();
}
setState(state) { this._state = state; }
getState() { return this._state; }
isInService() { return !(this._state instanceof OutOfServiceState); }
isFull() { return false; } // capacity tracking simplified for this demo
// Add a stop request; SCAN-style: keep the elevator moving in its current direction if possible.
addStop(floor) {
if (floor > this.currentFloor) this._upStops.add(floor);
else if (floor < this.currentFloor) this._downStops.add(floor);
if (this.direction === Direction.IDLE && (this._upStops.size > 0 || this._downStops.size > 0)) {
this.direction = this._upStops.size > 0 ? Direction.UP : Direction.DOWN;
this.setState(new MovingState());
}
}
// Advance the cabin by one floor toward its next pending stop (SCAN algorithm).
step() {
const name = this._state.name();
if (name !== 'MOVING' && name !== 'IDLE') return;
if (this.direction === Direction.UP) {
if (this._upStops.size === 0) {
this.direction = this._downStops.size === 0 ? Direction.IDLE : Direction.DOWN;
if (this.direction === Direction.IDLE) this.setState(new IdleState());
return;
}
this.currentFloor++;
if (this._upStops.has(this.currentFloor)) {
this._upStops.delete(this.currentFloor);
this._state.handleArrival(this);
}
} else if (this.direction === Direction.DOWN) {
if (this._downStops.size === 0) {
this.direction = this._upStops.size === 0 ? Direction.IDLE : Direction.UP;
if (this.direction === Direction.IDLE) this.setState(new IdleState());
return;
}
this.currentFloor--;
if (this._downStops.has(this.currentFloor)) {
this._downStops.delete(this.currentFloor);
this._state.handleArrival(this);
}
}
}
openDoor() {
if (!this._state.canOpenDoor()) throw new Error(`Cannot open doors in state ${this._state.name()}`);
this.doorState = DoorState.OPEN;
}
closeDoor() {
this.doorState = DoorState.CLOSED;
if (this._upStops.size > 0 || this._downStops.size > 0) this.setState(new MovingState());
else { this.direction = Direction.IDLE; this.setState(new IdleState()); }
}
setOutOfService() {
this.setState(new OutOfServiceState());
this.direction = Direction.IDLE;
}
pendingStops() {
return [...this._upStops, ...this._downStops];
}
}
// ---------- DispatchStrategy (interface, Strategy pattern) ----------
class DispatchStrategy {
selectElevator(_call, _elevators) { throw new Error('not implemented'); }
}
class NearestCarStrategy extends DispatchStrategy {
selectElevator(call, elevators) {
let best = null;
let bestDistance = Infinity;
for (const e of elevators) {
if (!e.isInService()) continue;
if (e.direction === call.direction || e.direction === Direction.IDLE) {
const d = Math.abs(e.currentFloor - call.floor);
if (d < bestDistance) { best = e; bestDistance = d; }
}
}
return best;
}
}
// Zones the building into bands; prefers an elevator already assigned near that band.
class ScanZoneStrategy extends DispatchStrategy {
constructor(zoneSize) { super(); this.zoneSize = zoneSize; }
selectElevator(call, elevators) {
const callZone = Math.floor(call.floor / this.zoneSize);
let best = null;
let bestScore = Infinity;
for (const e of elevators) {
if (!e.isInService()) continue;
const elevatorZone = Math.floor(e.currentFloor / this.zoneSize);
const score = Math.abs(elevatorZone - callZone) * 1000 + Math.abs(e.currentFloor - call.floor);
if (score < bestScore) { best = e; bestScore = score; }
}
return best;
}
}
// ---------- Dispatcher ----------
// Atomically assigns each incoming hall call to one elevator via the active strategy.
class Dispatcher {
constructor(elevators, strategy) {
this.elevators = elevators;
this.strategy = strategy;
}
setStrategy(strategy) { this.strategy = strategy; }
// Select then atomically claim; retries against remaining elevators if claim fails.
dispatch(hallCall) {
let remaining = [...this.elevators];
while (remaining.length > 0) {
const candidate = this.strategy.selectElevator(hallCall, remaining);
if (!candidate) return; // no elevator can currently serve this call
// Single synchronous critical section -- stands in for "lock(candidate)".
if (candidate.isInService() && !candidate.isFull()) {
candidate.addStop(hallCall.floor);
return; // claimed
}
remaining = remaining.filter((e) => e !== candidate);
}
}
}
// ---------- ElevatorController (facade) ----------
class ElevatorController {
constructor(elevatorList, strategy) {
this.elevators = new Map(elevatorList.map((e) => [e.id, e]));
this.dispatcher = new Dispatcher(elevatorList, strategy);
}
requestElevator(floor, dir) {
this.dispatcher.dispatch({ floor, direction: dir });
}
selectFloor(elevatorId, floor) {
const e = this._find(elevatorId);
e.addStop(floor); // car call: destination chosen from inside the cabin
}
setOutOfService(elevatorId) {
const e = this._find(elevatorId);
const pending = e.pendingStops();
e.setOutOfService();
// Redistribute this elevator's pending requests to the rest of the fleet.
for (const floor of pending) this.dispatcher.dispatch({ floor, direction: Direction.IDLE });
}
getStatus(elevatorId) {
const e = this._find(elevatorId);
return {
id: e.id, currentFloor: e.currentFloor, direction: e.direction,
doorState: e.doorState, state: e.getState().name(),
toString() { return `Elevator[${this.id}] floor=${this.currentFloor} dir=${this.direction} door=${this.doorState} state=${this.state}`; },
};
}
// Advance every in-service elevator by one simulated time-step. An elevator that just
// opened its doors spends this tick "boarding" and closes them, resuming motion next tick.
tick() {
for (const e of this.elevators.values()) {
if (e.getState() instanceof DoorsOpenState) e.closeDoor();
else e.step();
}
}
_find(elevatorId) {
const e = this.elevators.get(elevatorId);
if (!e) throw new Error(`No such elevator: ${elevatorId}`);
return e;
}
}
// ---------- Demo ----------
function main() {
const cabins = [new Elevator('E1', 8), new Elevator('E2', 8)];
const controller = new ElevatorController(cabins, new NearestCarStrategy());
controller.requestElevator(5, Direction.UP);
controller.requestElevator(2, Direction.DOWN);
controller.selectFloor('E1', 8);
for (let i = 0; i < 12; i++) controller.tick();
console.log(controller.getStatus('E1').toString());
console.log(controller.getStatus('E2').toString());
controller.setOutOfService('E2');
console.log('After maintenance: ' + controller.getStatus('E2').toString());
}
if (require.main === module) {
main();
}
module.exports = {
Direction, DoorState, Elevator, Dispatcher, ElevatorController,
NearestCarStrategy, ScanZoneStrategy, IdleState, MovingState, DoorsOpenState, OutOfServiceState,
};
/**
* Elevator System -- full class-diagram implementation.
*
* Elevator delegates behavior to its current ElevatorState (State pattern) so illegal
* transitions (e.g. opening doors while moving) are structurally unrepresentable.
* Dispatcher uses a swappable DispatchStrategy to select a candidate elevator, then
* atomically claims the hall call under a per-elevator critical section.
*/
enum Direction { UP = 'UP', DOWN = 'DOWN', IDLE = 'IDLE' }
enum DoorState { OPEN = 'OPEN', CLOSED = 'CLOSED' }
// ---------- Value objects ----------
interface HallCall { floor: number; direction: Direction; }
interface CarCall { destinationFloor: number; }
// ---------- ElevatorState (interface, State pattern) ----------
interface ElevatorState {
handleArrival(elevator: Elevator): void;
canOpenDoor(): boolean;
name(): string;
}
class IdleState implements ElevatorState {
handleArrival(_e: Elevator): void {}
canOpenDoor(): boolean { return true; }
name(): string { return 'IDLE'; }
}
class MovingState implements ElevatorState {
handleArrival(e: Elevator): void {
// The cabin reports arrival at a floor it must serve -> transition to DoorsOpenState.
e.setState(new DoorsOpenState());
e.openDoor();
}
canOpenDoor(): boolean { return false; } // cannot open while moving
name(): string { return 'MOVING'; }
}
class DoorsOpenState implements ElevatorState {
handleArrival(_e: Elevator): void {}
canOpenDoor(): boolean { return true; }
name(): string { return 'DOORS_OPEN'; }
}
class OutOfServiceState implements ElevatorState {
handleArrival(_e: Elevator): void {}
canOpenDoor(): boolean { return false; }
name(): string { return 'OUT_OF_SERVICE'; }
}
// ---------- Elevator ----------
// Owns one cabin's physical state and its own pending-stop queue.
class Elevator {
currentFloor = 0;
direction: Direction = Direction.IDLE;
doorState: DoorState = DoorState.CLOSED;
private state: ElevatorState = new IdleState();
private upStops = new Set<number>();
private downStops = new Set<number>();
constructor(readonly id: string, readonly capacity: number) {}
setState(state: ElevatorState): void { this.state = state; }
getState(): ElevatorState { return this.state; }
isInService(): boolean { return !(this.state instanceof OutOfServiceState); }
isFull(): boolean { return false; } // capacity tracking simplified for this demo
/** Add a stop request; SCAN-style: keep the elevator moving in its current direction if possible. */
addStop(floor: number): void {
if (floor > this.currentFloor) this.upStops.add(floor);
else if (floor < this.currentFloor) this.downStops.add(floor);
if (this.direction === Direction.IDLE && (this.upStops.size > 0 || this.downStops.size > 0)) {
this.direction = this.upStops.size > 0 ? Direction.UP : Direction.DOWN;
this.setState(new MovingState());
}
}
/** Advance the cabin by one floor toward its next pending stop (SCAN algorithm). */
step(): void {
const name = this.state.name();
if (name !== 'MOVING' && name !== 'IDLE') return;
if (this.direction === Direction.UP) {
if (this.upStops.size === 0) {
this.direction = this.downStops.size === 0 ? Direction.IDLE : Direction.DOWN;
if (this.direction === Direction.IDLE) this.setState(new IdleState());
return;
}
this.currentFloor++;
if (this.upStops.has(this.currentFloor)) {
this.upStops.delete(this.currentFloor);
this.state.handleArrival(this);
}
} else if (this.direction === Direction.DOWN) {
if (this.downStops.size === 0) {
this.direction = this.upStops.size === 0 ? Direction.IDLE : Direction.UP;
if (this.direction === Direction.IDLE) this.setState(new IdleState());
return;
}
this.currentFloor--;
if (this.downStops.has(this.currentFloor)) {
this.downStops.delete(this.currentFloor);
this.state.handleArrival(this);
}
}
}
openDoor(): void {
if (!this.state.canOpenDoor()) throw new Error(`Cannot open doors in state ${this.state.name()}`);
this.doorState = DoorState.OPEN;
}
closeDoor(): void {
this.doorState = DoorState.CLOSED;
if (this.upStops.size > 0 || this.downStops.size > 0) this.setState(new MovingState());
else { this.direction = Direction.IDLE; this.setState(new IdleState()); }
}
setOutOfService(): void {
this.setState(new OutOfServiceState());
this.direction = Direction.IDLE;
}
pendingStops(): number[] {
return [...this.upStops, ...this.downStops];
}
}
// ---------- DispatchStrategy (interface, Strategy pattern) ----------
interface DispatchStrategy {
selectElevator(call: HallCall, elevators: Elevator[]): Elevator | null;
}
class NearestCarStrategy implements DispatchStrategy {
selectElevator(call: HallCall, elevators: Elevator[]): Elevator | null {
let best: Elevator | null = null;
let bestDistance = Infinity;
for (const e of elevators) {
if (!e.isInService()) continue;
if (e.direction === call.direction || e.direction === Direction.IDLE) {
const d = Math.abs(e.currentFloor - call.floor);
if (d < bestDistance) { best = e; bestDistance = d; }
}
}
return best;
}
}
// Zones the building into bands; prefers an elevator already assigned near that band.
class ScanZoneStrategy implements DispatchStrategy {
constructor(private readonly zoneSize: number) {}
selectElevator(call: HallCall, elevators: Elevator[]): Elevator | null {
const callZone = Math.floor(call.floor / this.zoneSize);
let best: Elevator | null = null;
let bestScore = Infinity;
for (const e of elevators) {
if (!e.isInService()) continue;
const elevatorZone = Math.floor(e.currentFloor / this.zoneSize);
const score = Math.abs(elevatorZone - callZone) * 1000 + Math.abs(e.currentFloor - call.floor);
if (score < bestScore) { best = e; bestScore = score; }
}
return best;
}
}
// ---------- Dispatcher ----------
// Atomically assigns each incoming hall call to one elevator via the active strategy.
class Dispatcher {
constructor(private readonly elevators: Elevator[], private strategy: DispatchStrategy) {}
setStrategy(strategy: DispatchStrategy): void { this.strategy = strategy; }
/** Select then atomically claim; retries against remaining elevators if claim fails. */
dispatch(hallCall: HallCall): void {
let remaining = [...this.elevators];
while (remaining.length > 0) {
const candidate = this.strategy.selectElevator(hallCall, remaining);
if (!candidate) return; // no elevator can currently serve this call
// Single synchronous critical section -- stands in for "lock(candidate)".
if (candidate.isInService() && !candidate.isFull()) {
candidate.addStop(hallCall.floor);
return; // claimed
}
remaining = remaining.filter((e) => e !== candidate);
}
}
}
// ---------- ElevatorController (facade) ----------
interface ElevatorStatus {
id: string;
currentFloor: number;
direction: Direction;
doorState: DoorState;
state: string;
}
class ElevatorController {
private readonly elevators: Map<string, Elevator>;
private readonly dispatcher: Dispatcher;
constructor(elevatorList: Elevator[], strategy: DispatchStrategy) {
this.elevators = new Map(elevatorList.map((e) => [e.id, e]));
this.dispatcher = new Dispatcher(elevatorList, strategy);
}
requestElevator(floor: number, dir: Direction): void {
this.dispatcher.dispatch({ floor, direction: dir });
}
selectFloor(elevatorId: string, floor: number): void {
const e = this.find(elevatorId);
e.addStop(floor); // car call: destination chosen from inside the cabin
}
setOutOfService(elevatorId: string): void {
const e = this.find(elevatorId);
const pending = e.pendingStops();
e.setOutOfService();
// Redistribute this elevator's pending requests to the rest of the fleet.
for (const floor of pending) this.dispatcher.dispatch({ floor, direction: Direction.IDLE });
}
getStatus(elevatorId: string): ElevatorStatus {
const e = this.find(elevatorId);
return { id: e.id, currentFloor: e.currentFloor, direction: e.direction, doorState: e.doorState, state: e.getState().name() };
}
/**
* Advance every in-service elevator by one simulated time-step. An elevator that just
* opened its doors spends this tick "boarding" and closes them, resuming motion next tick.
*/
tick(): void {
for (const e of this.elevators.values()) {
if (e.getState() instanceof DoorsOpenState) e.closeDoor();
else e.step();
}
}
private find(elevatorId: string): Elevator {
const e = this.elevators.get(elevatorId);
if (!e) throw new Error(`No such elevator: ${elevatorId}`);
return e;
}
}
function statusToString(s: ElevatorStatus): string {
return `Elevator[${s.id}] floor=${s.currentFloor} dir=${s.direction} door=${s.doorState} state=${s.state}`;
}
// ---------- Demo ----------
function main(): void {
const cabins = [new Elevator('E1', 8), new Elevator('E2', 8)];
const controller = new ElevatorController(cabins, new NearestCarStrategy());
controller.requestElevator(5, Direction.UP);
controller.requestElevator(2, Direction.DOWN);
controller.selectFloor('E1', 8);
for (let i = 0; i < 12; i++) controller.tick();
console.log(statusToString(controller.getStatus('E1')));
console.log(statusToString(controller.getStatus('E2')));
controller.setOutOfService('E2');
console.log('After maintenance: ' + statusToString(controller.getStatus('E2')));
}
if (require.main === module) {
main();
}
export {
Direction, DoorState, Elevator, Dispatcher, ElevatorController,
DispatchStrategy, NearestCarStrategy, ScanZoneStrategy,
ElevatorState, IdleState, MovingState, DoorsOpenState, OutOfServiceState,
};