Skip to main content

Elevator System

An elevator's behavior genuinely depends on which of a handful of discrete states it is in - idle, moving, doors open - and the request that just arrived means something different in each one. That makes it a good showcase for the State pattern from the catalog: not because every machine-coding problem needs a pattern, but because this one's requirements already have pattern-shaped seams.

Requirements

Functional

  • A hall button (floor + direction) or a cabin button (destination floor) can request the elevator at any time.
  • The elevator serves requests in an order that makes sense - it should not reverse direction while requests remain ahead of it in the direction it's already moving.
  • Multiple elevators exist; a hall request should go to whichever one can reach that floor soonest, not always elevator #1.

Non-functional

  • Adding a new state (say, a maintenance/out-of-service mode) should not require touching the logic of the other states.
  • Dispatch (which elevator gets a request) should be swappable independent of how a single elevator processes its own request queue.

Design

Two decisions carry this design. First, Elevator never has an if (state == MOVING) anywhere in it - it holds a ElevatorState object and forwards every event to it. Second, picking which elevator answers a hall call is not the elevator's job at all; it belongs to ElevatorController, so a smarter dispatch algorithm later never touches Elevator.

Hall buttonElevatorControllerElevatorElevatorStaterequestElevator(5, UP)1pick nearest elevator2addRequest(5)3handleRequest(5)4transition to MovingState5step()6arrived: transition to DoorOpenState7
  1. 1A passenger on floor 5 wants to go up. The button only knows the controller.
  2. 2The controller picks the elevator with the smallest floor distance - this is the only class allowed to compare elevators to each other.
  3. 3The chosen elevator is told to serve floor 5; it does not know it was chosen over others.
  4. 4Elevator forwards the event to whatever state it currently holds - today that is IdleState.
  5. 5IdleState decides the elevator should move and swaps in a MovingState.
  6. 6Each tick, the elevator asks its current state what to do next - move one floor, or stop.
  7. 7On reaching floor 5, MovingState swaps in DoorOpenState; doors open without Elevator ever checking a floor number itself.

Class diagram

«interface»ElevatorState+ handleRequest(e, floor)+ step(e)ElevatorController- elevators: List<Elevator>+ requestElevator(floor, dir)Elevator- id: int- currentFloor: int- state: ElevatorState- requests: SortedSet<int>+ addRequest(floor)+ step()+ openDoors()+ closeDoors()IdleState+ handleRequest(e, floor)+ step(e)MovingState+ handleRequest(e, floor)+ step(e)DoorOpenState+ handleRequest(e, floor)+ step(e)
implementsuses
Elevator delegates to whichever ElevatorState it currently holds; the controller decides which elevator, never how it behaves.

Code

import java.util.*;
 
interface ElevatorState {
void handleRequest(Elevator elevator, int floor);
void step(Elevator elevator);
}
 
class IdleState implements ElevatorState {
public void handleRequest(Elevator elevator, int floor) {
elevator.addRequest(floor);
elevator.setState(new MovingState());
}
 
public void step(Elevator elevator) {
// Nothing to do until a request arrives.
}
}
 
class MovingState implements ElevatorState {
public void handleRequest(Elevator elevator, int floor) {
elevator.addRequest(floor);
}
 
public void step(Elevator elevator) {
int target = elevator.nextStop();
if (target == elevator.getCurrentFloor()) {
elevator.setState(new DoorOpenState());
return;
}
elevator.moveToward(target);
}
}
 
class DoorOpenState implements ElevatorState {
public void handleRequest(Elevator elevator, int floor) {
elevator.addRequest(floor);
}
 
public void step(Elevator elevator) {
elevator.removeRequest(elevator.getCurrentFloor());
elevator.setState(elevator.hasPendingRequests() ? new MovingState() : new IdleState());
}
}
 
class Elevator {
private final int id;
private int currentFloor = 0;
private ElevatorState state = new IdleState();
private final TreeSet<Integer> requests = new TreeSet<>();
 
Elevator(int id) {
this.id = id;
}
 
void addRequest(int floor) {
requests.add(floor);
}
 
void removeRequest(int floor) {
requests.remove(floor);
}
 
boolean hasPendingRequests() {
return !requests.isEmpty();
}
 
int nextStop() {
return requests.isEmpty() ? currentFloor : requests.first();
}
 
void moveToward(int target) {
currentFloor += target > currentFloor ? 1 : -1;
}
 
void handleRequest(int floor) {
state.handleRequest(this, floor);
}
 
void step() {
state.step(this);
}
 
void setState(ElevatorState state) {
this.state = state;
}
 
int getCurrentFloor() {
return currentFloor;
}
 
int getId() {
return id;
}
}
 
class ElevatorController {
private final List<Elevator> elevators;
 
ElevatorController(List<Elevator> elevators) {
this.elevators = elevators;
}
 
void requestElevator(int floor, String direction) {
Elevator best = elevators.stream()
.min(Comparator.comparingInt(e -> Math.abs(e.getCurrentFloor() - floor)))
.orElseThrow();
best.handleRequest(floor);
}
}

Design decisions

  • State pattern, not a status flag. An ElevatorStatus enum with if/switch blocks scattered across move(), requestFloor(), and openDoors() is where elevator bugs actually live in practice - one branch gets updated for a new state, another is forgotten. Giving each state its own class means the compiler (or at least a code reviewer) can see every place a new state has to plug in.
  • Dispatch lives in the controller, not the elevator. Elevator only knows how to react to its own requests; it has no idea other elevators exist. ElevatorController is the only class that compares elevators to each other, so "nearest elevator" can become "nearest elevator going the same direction" later without Elevator changing at all.
  • Requests are a sorted structure, not a plain list. Serving requests in floor order (rather than arrival order) is what keeps the elevator from bouncing - the sweep direction only reverses once there is nothing left ahead of it.
  • What's missing for a real system: starvation (a request in the opposite direction can wait through several sweeps under high load - a real answer ages requests or caps sweep length), and concurrent requests arriving while move() is mid-step need the request queue to be thread-safe, which this single-threaded walkthrough sidesteps.
0%0 of 122 pages studied