Practice · Low Level Design · State Machines
Design an Elevator System
কয়েকটা elevator, অনেক floor, অধৈর্য মানুষ: প্রতিটা elevator এর পেছনের state machine, আর একটা scheduler যে ঠিক করে কোন elevator কোন call এ যাবে।
Learn mode: পুরো solution নিচে দেখানো আছে।
Problem এর পরিচয়
| Difficulty | Intermediate |
| সময় | Design আর code মিলিয়ে ২–৩ ঘণ্টা; interview তে মোটামুটি ৪৫ মিনিট |
| আগে জানা দরকার | State machine, interface, lock |
| যা শিখবে | প্রতিটা lift এর জন্য একটা state machine, call ভাগ করার একটা Strategy, LOOK algorithm, simulate করা সময়, আলাদা goroutine এ button আর ticker |
| বাস্তবে কোথায় | অফিস, হাসপাতাল আর apartment এর lift; চলমান কর্মীদের request ভাগ করে দেয় এমন যেকোনো system (ride, delivery) |
Elevator এর প্রশ্ন দেখে তুমি একটা চলমান, একসাথে অনেক কাজ চলা system কে পরিষ্কার টুকরো তে ভাগ করতে পারো কিনা: প্রতিটা lift জানে কীভাবে চলতে হয় আর কোথায় থামবে বলে কথা দিয়েছে, আর একটা dispatcher ঠিক করে কোন lift একটা call নেবে। তারপর interviewer dispatch এর rule নিয়ে চাপ দেন, কারণ trade-off গুলো সেখানেই।
Problem statement
গুলশানে দশ তলার একটা অফিস building এ দুইটা lift, A আর B। কোনো floor এ দাঁড়ানো মানুষ up বা down চাপে (একে বলে hall call); lift এর ভেতরের মানুষ একটা floor নম্বর চাপে (car call)। প্রতিটা lift প্রতি tick এ এক floor চলে, কথা দেওয়া floor গুলোতে থামে, আর দরজা খোলে।
আমরা কী বানাচ্ছি: সেই controller, যেটা hall call গুলো lift দের ভাগ করে দেয় আর lift গুলো চালায়।
কেন জরুরি: খারাপ dispatcher দূরের lift পাঠায়, বা এমন lift পাঠায় যেটা উল্টো দিকে যাচ্ছে, আর মানুষ lobby তে মিনিটের পর মিনিট দাঁড়িয়ে থাকে।
এক লাইনে: প্রতিটা hall call সেই lift কে দাও যে সবচেয়ে তাড়াতাড়ি পৌঁছাতে পারে, আর প্রতিটা lift কে তার থামার জায়গা গুলো দিয়ে ভালোভাবে চালাও।
Requirement
Functional requirement
- N টা floor, M টা lift। প্রতিটা lift এর এখনকার floor, একটা দিক (idle, up, down) আর থামার জায়গার একটা set আছে।
- Hall call (floor, দিক): একটা lift বাছো আর তার থামার জায়গায় floor টা যোগ করো।
- Car call (lift, floor): ওই lift এর থামার জায়গায় floor টা যোগ করো।
- Tick: প্রতিটা lift পরের থামার জায়গার দিকে এক floor চলে, আর পৌঁছালে দরজা খোলে।
- একটা lift সামনে থামার জায়গা থাকা পর্যন্ত নিজের দিকেই চলতে থাকে, তারপর ঘোরে (এদিক-ওদিক না)।
- যে floor বা lift নেই, সেগুলো ফিরিয়ে দাও।
Non-functional requirement
| Requirement | এখানে এর মানে |
|---|---|
| কম অপেক্ষা | Dispatcher idle lift আর সঠিক দিকে ওই floor এর দিকে আসা lift কে আগে বাছে |
| Extensibility | Dispatch এর rule বদলানো (rush hour mode, VIP floor) lift গুলোতে হাত দেয় না |
| Concurrency তেও ঠিক থাকা | অনেক goroutine এ button চাপা হয়, আর একটা ticker lift চালায় |
| Testability | সময় হলো একটা Tick() call, তাই test এ sleep লাগে না |
যা ধরে নিচ্ছি আর যা এখানে নেই
- দরজা এক tick এর মধ্যেই খোলে আর বন্ধ হয়; কতজন উঠতে পারবে তার সীমা নেই, emergency বা maintenance mode নেই।
যা জমা দিতে হবে
- Entity গুলো, একটা lift এর state diagram, আর dispatch এর rule।
- Hall call, car call আর tick এর code।
- Dispatch, LOOK এর ক্রম আর concurrency র test।
Hint 1: কে ঠিক করবে?
কোন hall call নেবে, সেটা lift নিজে ঠিক করবে না; সে শুধু নিজেকে দেখে। সিদ্ধান্তটা রাখো এমন একটা object এ যে সব lift দেখে।
Hint 2: একটা lift এর state
Idle, উপরে যাচ্ছে, নিচে যাচ্ছে। কী হলে একটা থেকে আরেকটায় যায়?
Hint 3: কোন lift "সবচেয়ে কাছে"?
শুধু দূরত্ব যথেষ্ট না। দুই floor দূরের একটা lift যদি উল্টো দিকে যায়, পাঁচ floor দূরের যে lift তোমার দিকে আসছে তার চেয়ে অনেক বেশি সময় নিতে পারে।
Hint 4: সময়
আসল sleep ব্যবহার কোরো না। একটা Tick() method যেটা প্রতিটা lift কে এক floor চালায়, পুরো system টা test করার মতো বানিয়ে দেয়।
মূল entity
| Entity | এক বাক্যে কাজ | কী রাখে | কী করে |
|---|---|---|---|
Car | নিজের থামার জায়গা গুলো ধরে চলে | নাম, floor, দিক, থামার জায়গা | AddStop, Step (LOOK) |
Direction | Lift এর state | idle / up / down | কিছু না: একটা enum |
Dispatcher (interface) | Hall call এর জন্য একটা lift বাছে | rule অনুযায়ী | Pick(cars, floor, dir) |
Nearest | Default rule: দূরত্ব আর দিক ধরে সবচেয়ে সস্তা lift | floor সংখ্যা (penalty র জন্য) | cost, Pick |
Building | প্রবেশপথ; lift আর lock এর মালিক | floor, lift, dispatcher | Call, Press, Tick, Idle |
কে কার সাথে কীভাবে যুক্ত: Building তার lift গুলোর মালিক আর একটা Dispatcher ব্যবহার করে (Strategy)। প্রতিটা Car নিজের থামার জায়গার মালিক আর নিজের দিক নিজেই বদলায়। Hall call যায় dispatcher দিয়ে; car call সোজা lift এ যায়।
Diagram
Class diagram
কীভাবে পড়বে: Lift গুলোর মালিক Building, lock টাও তার কাছে। সে নিজে lift বাছে না: তার কাছে একটা Dispatcher আছে, তাকে Pick করতে বলে। আজকের rule হলো Nearest; rush-hour এর rule হলে সেটা একই interface implement করা আরেকটা box হতো। প্রতিটা Car এর নিজের থামার জায়গা আর নিজের Direction, যেটা LOOK diagram এর ছোট state machine টা।
চিহ্ন গুলো: + মানে public, - মানে private। ভরা diamond ◆ মানে মালিক (অংশটা মালিকের সাথেই আসে, সাথেই যায়), সাধারণ তীর মানে reference ধরে রাখে, dashed তীর মানে ব্যবহার করে, আর ফাঁপা ত্রিভুজওয়ালা dashed তীর মানে এই interface implement করে।
Sequence: একটা hall call, তারপর একটা tick
কীভাবে পড়বে: প্রতিটা খাড়া dashed লাইন একটা object, আর সময় চলে উপর থেকে নিচে। ভরা তীর মানে call, dashed তীর মানে উত্তর ফেরত আসা, আর সরু bar গুলো দেখায় কোন object কখন কাজে ব্যস্ত। মানুষ আর ticker দুইজন আলাদা caller, প্রায়ই আলাদা goroutine এ, আর দুজনেই Building দিয়ে যায়, এ জন্যই lock টা তার কাছে। Nearest শুধু একটা প্রশ্নের উত্তর দেয়; কোনো lift বদলায় না। বদলটা (AddStop) করে Building, lock ধরে রেখে।
একটা hall call ভাগ করে দেওয়া
কীভাবে পড়বে: প্রতিটা shaft একটা lift। খরচের rule idle lift বা caller এর দিকে ওই floor এর দিকে আসা lift এর জন্য শুধু দূরত্ব ধরে, আর উল্টো দিকে যাওয়া lift এর জন্য একটা penalty (building এর উচ্চতার দ্বিগুণ) যোগ করে। সবচেয়ে কম খরচ জেতে; সমান হলে প্রথম lift।
LOOK: একবারে একটা দিক
কীভাবে পড়বে: দুই দিকেই থামার জায়গা থাকলে lift আগে নিজের এখনকার দিকের সব কাজ শেষ করে, তারপর ঘোরে। এটাই LOOK algorithm (disk head ও এভাবে চলে): দূরের floor গুলো বঞ্চিত হয় না, আর সবসময় সবচেয়ে কাছের থামার জায়গায় যাওয়ার অপচয়ী আসা-যাওয়াও হয় না।
Design principle
- প্রতিটা lift নিজের চলার মালিক; dispatcher বাছাইয়ের মালিক। কেউ কারো data র ভেতরে হাত দেয় না।
- Open/closed: একটা rush-hour dispatcher (সকাল ৯টায় idle lift গুলো lobby তে পাঠাও) নতুন একটা
Dispatcher;CarআরBuildingবদলায় না। - সময় একটা input: production এ
Tick()ডাকে একটা ticker, test এ সরাসরি test। - Shared state এর জন্য একটা lock: button আর ticker দুজনেই
Buildingদিয়ে যায়, যে lock নেয়।
Design pattern
State (lift এর দিকের জন্য)
পরিষ্কার transition সহ তিনটা value এর একটা enum: থামার জায়গা যোগ হলে idle → up/down; সামনে কিছু না থাকলে up ↔ down; শেষ থামার জায়গা হয়ে গেলে up/down → idle। এখানে enum ই যথেষ্ট; সহজ আচরণের তিনটা state এর জন্য পুরো State pattern (প্রতি state এ একটা class) বাড়াবাড়ি।
Strategy (dispatch এর জন্য)
- কেন: "সবচেয়ে ভালো lift" এর rule building আর দিনের সময় অনুযায়ী বদলায়।
- কী সমাধান করে: dispatch হলো বদলানো যায় এমন একটা object,
Dispatcher। - না থাকলে:
Callএর ভেতরে একটাif rushHour { … }বাড়তে থাকে। - দাম: একটা interface; খরচ প্রায় নেই।
- কোথায়:
dispatcher.go;Building.Callজিজ্ঞেস করেb.dispatcher.Pick(...)।
Go তে পুরো solution
Project structure
elevator/
├── go.mod
├── car.go Car, Direction, the LOOK step
├── dispatcher.go the Dispatcher strategy and the Nearest rule
├── building.go Building: Call, Press, Tick, the lock
├── building_test.go tests, including buttons and ticks at once
└── cmd/demo/main.go a morning with two lifts
ধাপ ১: lift
// car.go
package elevator
import "sort"
// Direction is the car's state: a tiny state machine (Idle <-> Up/Down).
type Direction int
const (
Idle Direction = iota
Up
Down
)
func (d Direction) String() string { return [...]string{"idle", "up", "down"}[d] }
// Car keeps its own floor, direction and the floors it has promised to stop at.
type Car struct {
Name string
Floor int
Dir Direction
stops map[int]bool
}
func NewCar(name string, floor int) *Car {
return &Car{Name: name, Floor: floor, stops: map[int]bool{}}
}
func (c *Car) AddStop(floor int) {
if floor == c.Floor && c.Dir == Idle {
return
}
c.stops[floor] = true
if c.Dir == Idle {
c.Dir = c.towards(floor)
}
}
func (c *Car) towards(floor int) Direction {
switch {
case floor > c.Floor:
return Up
case floor < c.Floor:
return Down
}
return Idle
}
// hasStop reports whether any stop lies in direction d from the current floor.
func (c *Car) hasStop(d Direction) bool {
for f := range c.stops {
if (d == Up && f > c.Floor) || (d == Down && f < c.Floor) {
return true
}
}
return false
}
// Step moves one floor (the LOOK algorithm: keep going while there are stops ahead, then
// turn). It returns true if the car stopped at a floor it promised.
func (c *Car) Step() bool {
if len(c.stops) == 0 {
c.Dir = Idle
return false
}
if !c.hasStop(c.Dir) { // nothing ahead: turn around
if c.Dir == Up {
c.Dir = Down
} else {
c.Dir = Up
}
}
if c.Dir == Up {
c.Floor++
} else {
c.Floor--
}
if c.stops[c.Floor] {
delete(c.stops, c.Floor)
if len(c.stops) == 0 {
c.Dir = Idle
}
return true
}
return false
}
// Stops lists pending stops in order (for display and tests).
func (c *Car) Stops() []int {
out := []int{}
for f := range c.stops {
out = append(out, f)
}
sort.Ints(out)
return out
}
ধাপ ২: dispatcher
// dispatcher.go
package elevator
// Dispatcher is the Strategy that decides which car answers a hall call.
type Dispatcher interface {
Pick(cars []*Car, floor int, dir Direction) *Car
}
// Nearest prefers an idle car or one already heading to the floor in the caller's direction;
// a car going the other way is charged a penalty. Ties go to the first car.
type Nearest struct{ Floors int }
func (n Nearest) cost(c *Car, floor int, dir Direction) int {
dist := c.Floor - floor
if dist < 0 {
dist = -dist
}
switch {
case c.Dir == Idle:
return dist
case c.Dir == Up && dir == Up && floor >= c.Floor:
return dist
case c.Dir == Down && dir == Down && floor <= c.Floor:
return dist
}
return dist + 2*n.Floors // it must finish its trip and come back
}
func (n Nearest) Pick(cars []*Car, floor int, dir Direction) *Car {
best := cars[0]
for _, c := range cars[1:] {
if n.cost(c, floor, dir) < n.cost(best, floor, dir) {
best = c
}
}
return best
}
ধাপ ৩: building
// building.go
package elevator
import (
"fmt"
"sync"
)
// Building is the entry point: buttons call it from many goroutines, a ticker calls Tick.
type Building struct {
mu sync.Mutex
floors int
cars []*Car
dispatcher Dispatcher
}
func NewBuilding(floors int, d Dispatcher, cars ...*Car) *Building {
return &Building{floors: floors, cars: cars, dispatcher: d}
}
// Call is a hall button: someone on `floor` wants to go `dir`. It returns the car assigned.
func (b *Building) Call(floor int, dir Direction) (string, error) {
b.mu.Lock()
defer b.mu.Unlock()
if floor < 0 || floor >= b.floors {
return "", fmt.Errorf("no floor %d", floor)
}
c := b.dispatcher.Pick(b.cars, floor, dir)
c.AddStop(floor)
return c.Name, nil
}
// Press is a button inside a car.
func (b *Building) Press(car string, floor int) error {
b.mu.Lock()
defer b.mu.Unlock()
if floor < 0 || floor >= b.floors {
return fmt.Errorf("no floor %d", floor)
}
for _, c := range b.cars {
if c.Name == car {
c.AddStop(floor)
return nil
}
}
return fmt.Errorf("no car %s", car)
}
// Tick moves every car one floor and returns the cars that opened their doors.
func (b *Building) Tick() []string {
b.mu.Lock()
defer b.mu.Unlock()
var opened []string
for _, c := range b.cars {
if c.Step() {
opened = append(opened, fmt.Sprintf("car %s opens at %d", c.Name, c.Floor))
}
}
return opened
}
// Idle reports whether every car has nothing left to do, and where they are.
func (b *Building) Idle() (bool, string) {
b.mu.Lock()
defer b.mu.Unlock()
where := ""
for i, c := range b.cars {
if c.Dir != Idle {
return false, ""
}
if i > 0 {
where += ", "
}
where += fmt.Sprintf("%s at %d", c.Name, c.Floor)
}
return true, where
}
ধাপ ৪: চালিয়ে দেখো
// cmd/demo/main.go
package main
import (
"fmt"
"elevator"
)
func main() {
b := elevator.NewBuilding(10, elevator.Nearest{Floors: 10},
elevator.NewCar("A", 0), elevator.NewCar("B", 9))
call := func(t, floor int, dir elevator.Direction) {
car, _ := b.Call(floor, dir)
fmt.Printf("t%-2d call floor %d %-4s -> car %s\n", t, floor, dir, car)
}
press := func(t int, car string, floor int) {
b.Press(car, floor)
fmt.Printf("t%-2d car %s: passenger goes to %d\n", t, car, floor)
}
call(0, 3, elevator.Up)
call(0, 7, elevator.Down)
for t := 1; t <= 20; t++ {
for _, e := range b.Tick() {
fmt.Printf("t%-2d %s\n", t, e)
}
switch t { // what people do at each moment of the story
case 2:
press(t, "B", 1)
case 3:
press(t, "A", 8)
call(t, 5, elevator.Down)
}
if idle, where := b.Idle(); idle {
fmt.Printf("t%-2d all cars idle: %s\n", t, where)
return
}
}
}
$ go run ./cmd/demo
t0 call floor 3 up -> car A
t0 call floor 7 down -> car B
t2 car B opens at 7
t2 car B: passenger goes to 1
t3 car A opens at 3
t3 car A: passenger goes to 8
t3 call floor 5 down -> car B
t4 car B opens at 5
t8 car A opens at 8
t8 car B opens at 1
t8 all cars idle: A at 8, B at 1
Output টা পড়ি:
- t0: floor 3 up যায় A র কাছে (৩ floor দূরে, idle); floor 7 down যায় B র কাছে (২ floor দূরে, idle)।
- t2: B পৌঁছায় 7 এ; তার যাত্রী 1 চাপে।
- t3: A পৌঁছায় 3 এ; তার যাত্রী 8 চাপে। তারপর floor 5 থেকে down এর call: A উপরে যাচ্ছে (খরচ ২ + ২০), B আছে 6 এ নিচে যাচ্ছে (খরচ ১), তাই B পায়।
- t4: B পথেই 5 এ থামে।
- t8: একই tick এ A পৌঁছায় 8 এ আর B পৌঁছায় 1 এ; দুজনেই idle।
বাকি পাঁচটা language
from __future__ import annotations
import threading
from enum import Enum
from typing import Protocol
class Dir(Enum):
"""The car's state: a tiny state machine (IDLE <-> UP/DOWN)."""
IDLE = "idle"
UP = "up"
DOWN = "down"
class Car:
def __init__(self, name: str, floor: int):
self.name, self.floor, self.dir = name, floor, Dir.IDLE
self.stops: set[int] = set()
def add_stop(self, floor: int) -> None:
if floor == self.floor and self.dir is Dir.IDLE:
return
self.stops.add(floor)
if self.dir is Dir.IDLE:
self.dir = Dir.UP if floor > self.floor else Dir.DOWN
def _has_stop(self, d: Dir) -> bool:
return any((d is Dir.UP and f > self.floor) or (d is Dir.DOWN and f < self.floor) for f in self.stops)
def step(self) -> bool:
"""LOOK: keep going while there are stops ahead, then turn. True if it stopped."""
if not self.stops:
self.dir = Dir.IDLE
return False
if not self._has_stop(self.dir):
self.dir = Dir.DOWN if self.dir is Dir.UP else Dir.UP
self.floor += 1 if self.dir is Dir.UP else -1
if self.floor in self.stops:
self.stops.remove(self.floor)
if not self.stops:
self.dir = Dir.IDLE
return True
return False
class Dispatcher(Protocol):
"""The Strategy that decides which car answers a hall call."""
def pick(self, cars: list[Car], floor: int, d: Dir) -> Car: ...
class Nearest:
def __init__(self, floors: int):
self.floors = floors
def cost(self, c: Car, floor: int, d: Dir) -> int:
dist = abs(c.floor - floor)
if c.dir is Dir.IDLE:
return dist
if c.dir is Dir.UP and d is Dir.UP and floor >= c.floor:
return dist
if c.dir is Dir.DOWN and d is Dir.DOWN and floor <= c.floor:
return dist
return dist + 2 * self.floors # it must finish its trip and come back
def pick(self, cars: list[Car], floor: int, d: Dir) -> Car:
return min(cars, key=lambda c: self.cost(c, floor, d)) # min() keeps the first on ties
class Building:
def __init__(self, floors: int, dispatcher: Dispatcher, *cars: Car):
self._lock = threading.Lock()
self.floors, self.dispatcher, self.cars = floors, dispatcher, list(cars)
def call(self, floor: int, d: Dir) -> str:
with self._lock:
if not 0 <= floor < self.floors:
raise ValueError(f"no floor {floor}")
c = self.dispatcher.pick(self.cars, floor, d)
c.add_stop(floor)
return c.name
def press(self, car: str, floor: int) -> None:
with self._lock:
next(c for c in self.cars if c.name == car).add_stop(floor)
def tick(self) -> list[str]:
with self._lock:
return [f"car {c.name} opens at {c.floor}" for c in self.cars if c.step()]
def idle(self) -> tuple[bool, str]:
with self._lock:
if any(c.dir is not Dir.IDLE for c in self.cars):
return False, ""
return True, ", ".join(f"{c.name} at {c.floor}" for c in self.cars)
b = Building(10, Nearest(10), Car("A", 0), Car("B", 9))
def call(t: int, floor: int, d: Dir) -> None:
car = b.call(floor, d)
print(f"t{t:<2} call floor {floor} {d.value:<4} -> car {car}")
def press(t: int, car: str, floor: int) -> None:
b.press(car, floor)
print(f"t{t:<2} car {car}: passenger goes to {floor}")
call(0, 3, Dir.UP)
call(0, 7, Dir.DOWN)
for t in range(1, 21):
for e in b.tick():
print(f"t{t:<2} {e}")
if t == 2:
press(t, "B", 1)
elif t == 3:
press(t, "A", 8)
call(t, 5, Dir.DOWN)
done, where = b.idle()
if done:
print(f"t{t:<2} all cars idle: {where}")
break// The car's state: a tiny state machine (idle <-> up/down).
type Dir = "idle" | "up" | "down";
class Car {
readonly name: string;
floor: number;
dir: Dir = "idle";
readonly stops = new Set<number>();
constructor(name: string, floor: number) {
this.name = name;
this.floor = floor;
}
addStop(floor: number): void {
if (floor === this.floor && this.dir === "idle") return;
this.stops.add(floor);
if (this.dir === "idle") this.dir = floor > this.floor ? "up" : "down";
}
private hasStop(d: Dir): boolean {
for (const f of this.stops) if ((d === "up" && f > this.floor) || (d === "down" && f < this.floor)) return true;
return false;
}
// LOOK: keep going while there are stops ahead, then turn. True if it stopped.
step(): boolean {
if (this.stops.size === 0) {
this.dir = "idle";
return false;
}
if (!this.hasStop(this.dir)) this.dir = this.dir === "up" ? "down" : "up";
this.floor += this.dir === "up" ? 1 : -1;
if (this.stops.delete(this.floor)) {
if (this.stops.size === 0) this.dir = "idle";
return true;
}
return false;
}
}
// The Strategy that decides which car answers a hall call.
interface Dispatcher {
pick(cars: Car[], floor: number, d: Dir): Car;
}
class Nearest implements Dispatcher {
readonly floors: number;
constructor(floors: number) {
this.floors = floors;
}
cost(c: Car, floor: number, d: Dir): number {
const dist = Math.abs(c.floor - floor);
if (c.dir === "idle") return dist;
if (c.dir === "up" && d === "up" && floor >= c.floor) return dist;
if (c.dir === "down" && d === "down" && floor <= c.floor) return dist;
return dist + 2 * this.floors; // it must finish its trip and come back
}
pick(cars: Car[], floor: number, d: Dir): Car {
return cars.reduce((best, c) => (this.cost(c, floor, d) < this.cost(best, floor, d) ? c : best));
}
}
// No lock: one thread, and nothing here awaits.
class Building {
readonly floors: number;
readonly dispatcher: Dispatcher;
readonly cars: Car[];
constructor(floors: number, dispatcher: Dispatcher, ...cars: Car[]) {
this.floors = floors;
this.dispatcher = dispatcher;
this.cars = cars;
}
call(floor: number, d: Dir): string {
if (floor < 0 || floor >= this.floors) throw new Error(`no floor ${floor}`);
const c = this.dispatcher.pick(this.cars, floor, d);
c.addStop(floor);
return c.name;
}
press(car: string, floor: number): void {
this.cars.find((c) => c.name === car)!.addStop(floor);
}
tick(): string[] {
return this.cars.filter((c) => c.step()).map((c) => `car ${c.name} opens at ${c.floor}`);
}
idle(): [boolean, string] {
if (this.cars.some((c) => c.dir !== "idle")) return [false, ""];
return [true, this.cars.map((c) => `${c.name} at ${c.floor}`).join(", ")];
}
}
const b = new Building(10, new Nearest(10), new Car("A", 0), new Car("B", 9));
const tt = (t: number) => `t${String(t).padEnd(2)}`;
function call(t: number, floor: number, d: Dir): void {
console.log(`${tt(t)} call floor ${floor} ${d.padEnd(4)} -> car ${b.call(floor, d)}`);
}
function press(t: number, car: string, floor: number): void {
b.press(car, floor);
console.log(`${tt(t)} car ${car}: passenger goes to ${floor}`);
}
call(0, 3, "up");
call(0, 7, "down");
for (let t = 1; t <= 20; t++) {
for (const e of b.tick()) console.log(`${tt(t)} ${e}`);
if (t === 2) press(t, "B", 1);
if (t === 3) {
press(t, "A", 8);
call(t, 5, "down");
}
const [done, where] = b.idle();
if (done) {
console.log(`${tt(t)} all cars idle: ${where}`);
break;
}
}use std::collections::BTreeSet;
use std::fmt;
use std::sync::Mutex;
// The car's state: a tiny state machine (Idle <-> Up/Down).
#[derive(Clone, Copy, PartialEq)]
enum Dir { Idle, Up, Down }
impl fmt::Display for Dir {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(match self { Dir::Idle => "idle", Dir::Up => "up", Dir::Down => "down" })
}
}
struct Car { name: &'static str, floor: i32, dir: Dir, stops: BTreeSet<i32> }
impl Car {
fn new(name: &'static str, floor: i32) -> Car { Car { name, floor, dir: Dir::Idle, stops: BTreeSet::new() } }
fn add_stop(&mut self, floor: i32) {
if floor == self.floor && self.dir == Dir::Idle { return; }
self.stops.insert(floor);
if self.dir == Dir::Idle { self.dir = if floor > self.floor { Dir::Up } else { Dir::Down }; }
}
fn has_stop(&self, d: Dir) -> bool {
self.stops.iter().any(|&f| (d == Dir::Up && f > self.floor) || (d == Dir::Down && f < self.floor))
}
// LOOK: keep going while there are stops ahead, then turn. True if it stopped.
fn step(&mut self) -> bool {
if self.stops.is_empty() { self.dir = Dir::Idle; return false; }
if !self.has_stop(self.dir) { self.dir = if self.dir == Dir::Up { Dir::Down } else { Dir::Up }; }
self.floor += if self.dir == Dir::Up { 1 } else { -1 };
if self.stops.remove(&self.floor) {
if self.stops.is_empty() { self.dir = Dir::Idle; }
return true;
}
false
}
}
// The Strategy that decides which car answers a hall call (returns an index into cars).
trait Dispatcher: Send { fn pick(&self, cars: &[Car], floor: i32, d: Dir) -> usize; }
struct Nearest { floors: i32 }
impl Nearest {
fn cost(&self, c: &Car, floor: i32, d: Dir) -> i32 {
let dist = (c.floor - floor).abs();
match (c.dir, d) {
(Dir::Idle, _) => dist,
(Dir::Up, Dir::Up) if floor >= c.floor => dist,
(Dir::Down, Dir::Down) if floor <= c.floor => dist,
_ => dist + 2 * self.floors, // it must finish its trip and come back
}
}
}
impl Dispatcher for Nearest {
fn pick(&self, cars: &[Car], floor: i32, d: Dir) -> usize {
let mut best = 0;
for i in 1..cars.len() { if self.cost(&cars[i], floor, d) < self.cost(&cars[best], floor, d) { best = i; } }
best
}
}
struct Building { floors: i32, dispatcher: Box<dyn Dispatcher>, cars: Mutex<Vec<Car>> }
impl Building {
fn call(&self, floor: i32, d: Dir) -> Result<&'static str, String> {
if floor < 0 || floor >= self.floors { return Err(format!("no floor {floor}")); }
let mut cars = self.cars.lock().unwrap();
let i = self.dispatcher.pick(&cars, floor, d);
cars[i].add_stop(floor);
Ok(cars[i].name)
}
fn press(&self, car: &str, floor: i32) {
let mut cars = self.cars.lock().unwrap();
if let Some(c) = cars.iter_mut().find(|c| c.name == car) { c.add_stop(floor); }
}
fn tick(&self) -> Vec<String> {
let mut cars = self.cars.lock().unwrap();
cars.iter_mut().filter_map(|c| c.step().then(|| format!("car {} opens at {}", c.name, c.floor))).collect()
}
fn idle(&self) -> Option<String> {
let cars = self.cars.lock().unwrap();
if cars.iter().any(|c| c.dir != Dir::Idle) { return None; }
Some(cars.iter().map(|c| format!("{} at {}", c.name, c.floor)).collect::<Vec<_>>().join(", "))
}
}
fn main() {
let b = Building { floors: 10, dispatcher: Box::new(Nearest { floors: 10 }), cars: Mutex::new(vec![Car::new("A", 0), Car::new("B", 9)]) };
let call = |t: i32, floor: i32, d: Dir| {
let car = b.call(floor, d).unwrap();
println!("t{t:<2} call floor {floor} {d:<4} -> car {car}");
};
let press = |t: i32, car: &str, floor: i32| {
b.press(car, floor);
println!("t{t:<2} car {car}: passenger goes to {floor}");
};
call(0, 3, Dir::Up);
call(0, 7, Dir::Down);
for t in 1..=20 {
for e in b.tick() { println!("t{t:<2} {e}"); }
match t {
2 => press(t, "B", 1),
3 => { press(t, "A", 8); call(t, 5, Dir::Down); }
_ => {}
}
if let Some(where_) = b.idle() { println!("t{t:<2} all cars idle: {where_}"); break; }
}
}import java.util.List;
import java.util.TreeSet;
import java.util.stream.Collectors;
public class Main {
// The car's state: a tiny state machine (IDLE <-> UP/DOWN).
enum Dir {
IDLE, UP, DOWN;
@Override public String toString() { return name().toLowerCase(); }
}
static final class Car {
final String name;
int floor;
Dir dir = Dir.IDLE;
final TreeSet<Integer> stops = new TreeSet<>();
Car(String name, int floor) { this.name = name; this.floor = floor; }
void addStop(int f) {
if (f == floor && dir == Dir.IDLE) return;
stops.add(f);
if (dir == Dir.IDLE) dir = f > floor ? Dir.UP : Dir.DOWN;
}
boolean hasStop(Dir d) {
return d == Dir.UP ? stops.higher(floor) != null : stops.lower(floor) != null;
}
// LOOK: keep going while there are stops ahead, then turn. True if it stopped.
boolean step() {
if (stops.isEmpty()) { dir = Dir.IDLE; return false; }
if (!hasStop(dir)) dir = dir == Dir.UP ? Dir.DOWN : Dir.UP;
floor += dir == Dir.UP ? 1 : -1;
if (stops.remove(floor)) {
if (stops.isEmpty()) dir = Dir.IDLE;
return true;
}
return false;
}
}
// The Strategy that decides which car answers a hall call.
interface Dispatcher { Car pick(List<Car> cars, int floor, Dir d); }
record Nearest(int floors) implements Dispatcher {
int cost(Car c, int floor, Dir d) {
int dist = Math.abs(c.floor - floor);
if (c.dir == Dir.IDLE) return dist;
if (c.dir == Dir.UP && d == Dir.UP && floor >= c.floor) return dist;
if (c.dir == Dir.DOWN && d == Dir.DOWN && floor <= c.floor) return dist;
return dist + 2 * floors; // it must finish its trip and come back
}
public Car pick(List<Car> cars, int floor, Dir d) {
Car best = cars.get(0);
for (Car c : cars) if (cost(c, floor, d) < cost(best, floor, d)) best = c;
return best;
}
}
static final class Building {
private final int floors;
private final Dispatcher dispatcher;
private final List<Car> cars;
Building(int floors, Dispatcher d, Car... cars) { this.floors = floors; this.dispatcher = d; this.cars = List.of(cars); }
synchronized String call(int floor, Dir d) {
if (floor < 0 || floor >= floors) throw new IllegalArgumentException("no floor " + floor);
Car c = dispatcher.pick(cars, floor, d);
c.addStop(floor);
return c.name;
}
synchronized void press(String car, int floor) {
cars.stream().filter(c -> c.name.equals(car)).findFirst().orElseThrow().addStop(floor);
}
synchronized List<String> tick() {
return cars.stream().filter(Car::step).map(c -> "car " + c.name + " opens at " + c.floor).toList();
}
synchronized String idle() {
if (cars.stream().anyMatch(c -> c.dir != Dir.IDLE)) return null;
return cars.stream().map(c -> c.name + " at " + c.floor).collect(Collectors.joining(", "));
}
}
static Building b = new Building(10, new Nearest(10), new Car("A", 0), new Car("B", 9));
static void call(int t, int floor, Dir d) {
System.out.printf("t%-2d call floor %d %-4s -> car %s%n", t, floor, d, b.call(floor, d));
}
static void press(int t, String car, int floor) {
b.press(car, floor);
System.out.printf("t%-2d car %s: passenger goes to %d%n", t, car, floor);
}
public static void main(String[] args) {
call(0, 3, Dir.UP);
call(0, 7, Dir.DOWN);
for (int t = 1; t <= 20; t++) {
for (String e : b.tick()) System.out.printf("t%-2d %s%n", t, e);
if (t == 2) press(t, "B", 1);
if (t == 3) { press(t, "A", 8); call(t, 5, Dir.DOWN); }
String where = b.idle();
if (where != null) { System.out.printf("t%-2d all cars idle: %s%n", t, where); break; }
}
}
}#include <cstdio>
#include <cstdlib>
#include <memory>
#include <mutex>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>
#include <vector>
// The car's state: a tiny state machine (Idle <-> Up/Down).
enum class Dir { Idle, Up, Down };
const char* name(Dir d) { return d == Dir::Up ? "up" : d == Dir::Down ? "down" : "idle"; }
struct Car {
Car(std::string n, int f) : name(std::move(n)), floor(f) {}
std::string name;
int floor;
Dir dir = Dir::Idle;
std::set<int> stops;
void addStop(int f) {
if (f == floor && dir == Dir::Idle) return;
stops.insert(f);
if (dir == Dir::Idle) dir = f > floor ? Dir::Up : Dir::Down;
}
bool hasStop(Dir d) const {
return d == Dir::Up ? stops.upper_bound(floor) != stops.end() : stops.lower_bound(floor) != stops.begin();
}
// LOOK: keep going while there are stops ahead, then turn. True if it stopped.
bool step() {
if (stops.empty()) {
dir = Dir::Idle;
return false;
}
if (!hasStop(dir)) dir = dir == Dir::Up ? Dir::Down : Dir::Up;
floor += dir == Dir::Up ? 1 : -1;
if (stops.erase(floor)) {
if (stops.empty()) dir = Dir::Idle;
return true;
}
return false;
}
};
// The Strategy that decides which car answers a hall call.
class Dispatcher {
public:
virtual ~Dispatcher() = default;
virtual size_t pick(const std::vector<Car>& cars, int floor, Dir d) const = 0;
};
class Nearest : public Dispatcher {
public:
explicit Nearest(int floors) : floors_(floors) {}
int cost(const Car& c, int floor, Dir d) const {
int dist = std::abs(c.floor - floor);
if (c.dir == Dir::Idle) return dist;
if (c.dir == Dir::Up && d == Dir::Up && floor >= c.floor) return dist;
if (c.dir == Dir::Down && d == Dir::Down && floor <= c.floor) return dist;
return dist + 2 * floors_; // it must finish its trip and come back
}
size_t pick(const std::vector<Car>& cars, int floor, Dir d) const override {
size_t best = 0;
for (size_t i = 1; i < cars.size(); i++)
if (cost(cars[i], floor, d) < cost(cars[best], floor, d)) best = i;
return best;
}
private:
int floors_;
};
class Building {
public:
Building(int floors, std::unique_ptr<Dispatcher> d, std::vector<Car> cars)
: floors_(floors), dispatcher_(std::move(d)), cars_(std::move(cars)) {}
std::string call(int floor, Dir d) {
std::lock_guard lock(mu_);
if (floor < 0 || floor >= floors_) throw std::invalid_argument("no floor " + std::to_string(floor));
Car& c = cars_[dispatcher_->pick(cars_, floor, d)];
c.addStop(floor);
return c.name;
}
void press(const std::string& car, int floor) {
std::lock_guard lock(mu_);
for (auto& c : cars_)
if (c.name == car) c.addStop(floor);
}
std::vector<std::string> tick() {
std::lock_guard lock(mu_);
std::vector<std::string> opened;
for (auto& c : cars_)
if (c.step()) opened.push_back("car " + c.name + " opens at " + std::to_string(c.floor));
return opened;
}
std::optional<std::string> idle() {
std::lock_guard lock(mu_);
std::string where;
for (const auto& c : cars_) {
if (c.dir != Dir::Idle) return std::nullopt;
where += (where.empty() ? "" : ", ") + c.name + " at " + std::to_string(c.floor);
}
return where;
}
private:
std::mutex mu_;
int floors_;
std::unique_ptr<Dispatcher> dispatcher_;
std::vector<Car> cars_;
};
int main() {
Building b(10, std::make_unique<Nearest>(10), {Car{"A", 0}, Car{"B", 9}});
auto call = [&](int t, int floor, Dir d) {
std::string car = b.call(floor, d);
std::printf("t%-2d call floor %d %-4s -> car %s\n", t, floor, name(d), car.c_str());
};
auto press = [&](int t, const std::string& car, int floor) {
b.press(car, floor);
std::printf("t%-2d car %s: passenger goes to %d\n", t, car.c_str(), floor);
};
call(0, 3, Dir::Up);
call(0, 7, Dir::Down);
for (int t = 1; t <= 20; t++) {
for (const auto& e : b.tick()) std::printf("t%-2d %s\n", t, e.c_str());
if (t == 2) press(t, "B", 1);
if (t == 3) {
press(t, "A", 8);
call(t, 5, Dir::Down);
}
if (auto where = b.idle()) {
std::printf("t%-2d all cars idle: %s\n", t, where->c_str());
break;
}
}
}Language ভেদে কী আলাদা: Java আর C++ ক্রম রাখা set ব্যবহার করে (TreeSet, std::set), তাই "আমার উপরে কি কোনো থামার জায়গা আছে?" এক call এ হয় (higher, upper_bound)। Rust এর dispatcher reference এর বদলে একটা index ফেরত দেয়, যাতে একই lock এর ভেতরে lift বাছাই করে তারপর বদলানো যায়। Rust এর guard সহ match ((Dir::Up, Dir::Up) if floor >= c.floor) প্রায় খরচের table এর মতোই পড়া যায়। TypeScript এর lock লাগে না।
Testing
// building_test.go
package elevator
import (
"slices"
"sync"
"testing"
)
func TestNearestIdleCarWins(t *testing.T) {
b := NewBuilding(10, Nearest{10}, NewCar("A", 0), NewCar("B", 9))
if car, _ := b.Call(7, Down); car != "B" {
t.Fatalf("got %s", car)
}
if car, _ := b.Call(1, Up); car != "A" {
t.Fatalf("got %s", car)
}
}
func TestCarGoingTheWrongWayIsAvoided(t *testing.T) {
a, b := NewCar("A", 4), NewCar("B", 0)
a.AddStop(9) // A is going up, past floor 3
bld := NewBuilding(10, Nearest{10}, a, b)
if car, _ := bld.Call(3, Down); car != "B" {
t.Fatalf("A is heading away; got %s", car)
}
}
func TestLookServesStopsAheadFirst(t *testing.T) {
c := NewCar("A", 5)
c.AddStop(8) // going up
c.AddStop(2) // behind it
var order []int
for range 20 {
if c.Step() {
order = append(order, c.Floor)
}
}
if !slices.Equal(order, []int{8, 2}) || c.Dir != Idle {
t.Fatalf("order %v, dir %v", order, c.Dir)
}
}
func TestInvalidFloorRefused(t *testing.T) {
b := NewBuilding(10, Nearest{10}, NewCar("A", 0))
if _, err := b.Call(12, Up); err == nil {
t.Fatal("floor 12 should be refused")
}
if err := b.Press("Z", 3); err == nil {
t.Fatal("unknown car should be refused")
}
}
func TestEveryCallIsEventuallyServed(t *testing.T) {
b := NewBuilding(10, Nearest{10}, NewCar("A", 0), NewCar("B", 9))
for _, f := range []int{2, 5, 8, 1, 6} {
b.Call(f, Up)
}
for range 40 {
b.Tick()
}
if idle, _ := b.Idle(); !idle {
t.Fatal("cars still busy after 40 ticks")
}
}
// Buttons pressed from many goroutines while the ticker runs. Run with -race.
func TestConcurrentButtonsAndTicks(t *testing.T) {
b := NewBuilding(10, Nearest{10}, NewCar("A", 0), NewCar("B", 9))
var wg sync.WaitGroup
for i := range 30 {
wg.Add(2)
go func() { defer wg.Done(); b.Call(i%10, Up) }()
go func() { defer wg.Done(); b.Tick() }()
}
wg.Wait()
for range 60 {
b.Tick()
}
if idle, _ := b.Idle(); !idle {
t.Fatal("not idle")
}
}
$ go test -race -v ./...
=== RUN TestNearestIdleCarWins
--- PASS: TestNearestIdleCarWins (0.00s)
=== RUN TestCarGoingTheWrongWayIsAvoided
--- PASS: TestCarGoingTheWrongWayIsAvoided (0.00s)
=== RUN TestLookServesStopsAheadFirst
--- PASS: TestLookServesStopsAheadFirst (0.00s)
=== RUN TestInvalidFloorRefused
--- PASS: TestInvalidFloorRefused (0.00s)
=== RUN TestEveryCallIsEventuallyServed
--- PASS: TestEveryCallIsEventuallyServed (0.00s)
=== RUN TestConcurrentButtonsAndTicks
--- PASS: TestConcurrentButtonsAndTicks (0.00s)
PASS
ok elevator 1.015s
? elevator/cmd/demo [no test files]
| Test | যা check করে |
|---|---|
TestNearestIdleCarWins | Call যায় সবচেয়ে কাছের idle lift এ |
TestCarGoingTheWrongWayIsAvoided | দূরে চলে যাওয়া কাছের lift, দূরের idle lift এর কাছে হারে |
TestLookServesStopsAheadFirst | 5 থেকে, থামার জায়গা 8 আর 2 হলে ক্রম হলো 8 তারপর 2 |
TestInvalidFloorRefused | Floor 12 আর অচেনা lift ফিরিয়ে দেওয়া হয় |
TestEveryCallIsEventuallyServed | পাঁচটা call, আর ৪০ tick এর মধ্যে দুইটা lift ই idle |
TestConcurrentButtonsAndTicks | অনেক goroutine থেকে একসাথে call আর tick |
Tick থেকে lock সরালে race detector ২৮টা data race দেখিয়েছে, আর concurrency test fail করেছে।
Design review
যা ভালো: চলা (lift) আর বাছাই (dispatcher) পরিষ্কার আলাদা; সময় test করা যায়; খরচের rule বোঝানো আর বদলানো সহজ।
| "যদি…" | কী বদলাতে হবে |
|---|---|
| Rush hour | নতুন একটা dispatcher যেটা idle lift lobby তে পাঠায় আর floor 0 এর up-call কে আগে রাখে। |
| Lift এর ধারণক্ষমতা | Lift জানায় কতজন আছে; dispatcher ভরা lift বাদ দেয়। |
| Destination dispatch (lobby তেই floor বাছা) | Call এ গন্তব্য থাকে, তাই dispatcher কাছাকাছি floor এ যাওয়া মানুষ দের এক lift এ তুলতে পারে। |
| Call নতুন করে ভাগ করা | আরো ভালো একটা lift ফাঁকা হলে hall call সরিয়ে দাও; hall call গুলো শুধু lift এ না, building এও রাখো। |
| Emergency / আগুন mode | Building জুড়ে একটা state, যেটা সব lift কে নিচতলায় পাঠায় আর call উপেক্ষা করে। |
Interview প্রশ্ন
Practice প্রশ্ন
Beginner
- কোন lift call নেবে, সেটা lift না, dispatcher কেন ঠিক করে?
- একটা lift এর state কী কী, আর কী তাকে একটা থেকে আরেকটায় নেয়?
Intermediate
- শুধু "সবচেয়ে কাছের lift" কেন যথেষ্ট না?
- Sleep না করে একটা চলমান system কীভাবে test করবে?
Advanced
- LOOK আর সবসময় সবচেয়ে কাছের থামার জায়গায় যাওয়া, তুলনা করো।
- Button আর ticker আলাদা goroutine এ চলে। কী গোলমাল হতে পারে?
Design এর সাধারণ ভুল
- প্রতিটা lift নিজে নিজে call নিয়ে নেয়, ফলে একজনের জন্য দুইটা lift আসে।
- শুধু দূরত্ব দেখে dispatch, উল্টো দিকে যাওয়া lift পাঠানো।
- Logic এর ভেতরে আসল
time.Sleep, test ধীর আর unreliable। - Button আর ticker এর মাঝে কোনো lock নেই।
উত্তর
১. Dispatcher কেন?
সব lift দেখতে পায় এমন কেউই শুধু তাদের তুলনা করতে পারে। Lift রা নিজেরা বাছলে দুইজন একই call নিতে পারে, বা কেউই না।
২. একটা lift এর state?
Idle, up, down। থামার জায়গা যোগ হলে idle থেকে up বা down; সামনের শেষ থামার জায়গা হয়ে গেলে পেছনে থাকলে ঘোরে, না থাকলে idle হয়।
৩. শুধু সবচেয়ে কাছের lift কেন না?
দূরে চলে যাওয়া কাছের lift কে আগে নিজের trip শেষ করতে হয়। খরচের rule এর জন্য একটা penalty যোগ করে, তাই দূরের idle বা কাছে আসা lift জিততে পারে।
৪. Sleep ছাড়া test?
সময়কে একটা পরিষ্কার Tick() বানাও। Test এটাকে loop এ ডাকে আর দেখে কোন lift কোথায়; production এ ডাকে একটা ticker।
৫. LOOK বনাম সবচেয়ে কাছের থামার জায়গা?
সবচেয়ে কাছেরটায় গেলে কাছাকাছি floor গুলোর মধ্যে আসা-যাওয়া চলতে থাকে আর দূরের floor বঞ্চিত হয়। LOOK একটা দিক শেষ করে তারপর ঘোরে: অনুমান করা যায়, ন্যায্য, আর ঘোরা কম।
৬. Lock ছাড়া button বনাম ticker?
একটা call ঠিক তখন থামার জায়গা যোগ করতে পারে যখন Step একই map থেকে মুছছে (data race, আর Go তে crash ও হতে পারে), বা lift এর পুরনো অবস্থা দেখে দুইটা call ভাগ হতে পারে। Lock ছাড়া আমাদের test ২৮টা data race দেখিয়েছে।
Interviewer রা যা দেখেন: lift আর dispatcher এর দায়িত্ব আলাদা, বুঝেশুনে একটা খরচের rule, LOOK, test করা যায় এমন সময়, আর thread safety।
নিজে design করে solution টা বুঝেছো? Mark করো, তাহলে progress ট্র্যাক হবে।