সব problem (16)
System Design courseSystem Design Problems

0%

0/16 done

Practice · Low Level Design · State Machines

Design an Elevator System

IntermediateState machineSchedulingObserverStrategy⏱ 2-3 ঘণ্টা

কয়েকটা elevator, অনেক floor, অধৈর্য মানুষ: প্রতিটা elevator এর পেছনের state machine, আর একটা scheduler যে ঠিক করে কোন elevator কোন call এ যাবে।

Learn mode: পুরো solution নিচে দেখানো আছে।

Problem এর পরিচয়

DifficultyIntermediate
সময়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

  1. N টা floor, M টা lift। প্রতিটা lift এর এখনকার floor, একটা দিক (idle, up, down) আর থামার জায়গার একটা set আছে।
  2. Hall call (floor, দিক): একটা lift বাছো আর তার থামার জায়গায় floor টা যোগ করো।
  3. Car call (lift, floor): ওই lift এর থামার জায়গায় floor টা যোগ করো।
  4. Tick: প্রতিটা lift পরের থামার জায়গার দিকে এক floor চলে, আর পৌঁছালে দরজা খোলে।
  5. একটা lift সামনে থামার জায়গা থাকা পর্যন্ত নিজের দিকেই চলতে থাকে, তারপর ঘোরে (এদিক-ওদিক না)।
  6. যে floor বা lift নেই, সেগুলো ফিরিয়ে দাও।

Non-functional requirement

Requirementএখানে এর মানে
কম অপেক্ষাDispatcher idle lift আর সঠিক দিকে ওই floor এর দিকে আসা lift কে আগে বাছে
ExtensibilityDispatch এর 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)
DirectionLift এর stateidle / up / downকিছু না: একটা enum
Dispatcher (interface)Hall call এর জন্য একটা lift বাছেrule অনুযায়ীPick(cars, floor, dir)
NearestDefault rule: দূরত্ব আর দিক ধরে সবচেয়ে সস্তা liftfloor সংখ্যা (penalty র জন্য)cost, Pick
Buildingপ্রবেশপথ; lift আর lock এর মালিকfloor, lift, dispatcherCall, Press, Tick, Idle

কে কার সাথে কীভাবে যুক্ত: Building তার lift গুলোর মালিক আর একটা Dispatcher ব্যবহার করে (Strategy)। প্রতিটা Car নিজের থামার জায়গার মালিক আর নিজের দিক নিজেই বদলায়। Hall call যায় dispatcher দিয়ে; car call সোজা lift এ যায়।

Diagram

Class diagram

Building- floors: int- cars: []*Car- dispatcher: Dispatcher- mu: sync.Mutex+ Call(floor, dir) (string, error)+ Press(car, floor) error+ Tick() []string+ Idle() (bool, string)Car+ Name: string+ Floor: int+ Dir: Direction- stops: map[int]bool+ AddStop(floor)+ Step() bool- hasStop(dir) bool«interface»Dispatcher+ Pick(cars, floor, dir) *CarNearest+ Floors: int+ Pick(cars, floor, dir) *Car- cost(car, floor, dir) int«enum»DirectionIdleUpDowncars1*dispatcherDirpicksBuilding owns the cars and the one lockBuilding asks a Dispatcher which car takes a callNearest is one rule; a rush-hour rule would be another classeach Car keeps its own direction: idle, up or down
Play চাপো। আগে Building আর তার নিজের lift গুলো, তারপর সে যে Dispatcher কে জিজ্ঞেস করে, তারপর ওই interface এর পেছনে একটা rule হিসেবে Nearest, শেষে প্রতিটা lift এর Direction।

কীভাবে পড়বে: 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

PersonTickerBuildingNearestCar BCall(5, down)lockPick(cars, 5, down)A: 22, B: 1car BAddStop(5)"B"Tick()Step()true: doors open at 5
Play চাপো। ৫ তলা থেকে down call আসে। Building, Nearest কে জিজ্ঞেস করে, সে দুইটা lift এর খরচ হিসাব করে (A: 22, B: 1) আর B কে ফেরত দেয়। B থামার জায়গা পায়। পরের tick গুলোতে ticker Tick ডাকে, B এগোয় আর ৫ এ দরজা খোলে।

কীভাবে পড়বে: প্রতিটা খাড়া dashed লাইন একটা object, আর সময় চলে উপর থেকে নিচে। ভরা তীর মানে call, dashed তীর মানে উত্তর ফেরত আসা, আর সরু bar গুলো দেখায় কোন object কখন কাজে ব্যস্ত। মানুষ আর ticker দুইজন আলাদা caller, প্রায়ই আলাদা goroutine এ, আর দুজনেই Building দিয়ে যায়, এ জন্যই lock টা তার কাছে। Nearest শুধু একটা প্রশ্নের উত্তর দেয়; কোনো lift বদলায় না। বদলটা (AddStop) করে Building, lock ধরে রেখে।

একটা hall call ভাগ করে দেওয়া

car A shaftcar B shaftfloor 9floor 8floor 7floor 6floor 5floor 4floor 3floor 2floor 1floor 0A ↑ to 8B ↓ to 1call 5 ↓cost 2 + 20: wrong waycost 1: on its wayBthe dispatcher strategy picks car B; B stops at 5 on its way down
Play চাপো। Floor 5 থেকে "down" এর call আসে। Lift A আছে 3 এ, উপরে 8 এ যাচ্ছে, তাই তাকে সেই trip শেষ করে ফিরে আসতে হবে: অনেক খরচ। Lift B আছে 6 এ, নিচে 1 এ যাচ্ছে, আর floor 5 তার পথেই: খরচ 1। Dispatcher B কে বাছে, আর B নামার পথে 5 এ থামে।

কীভাবে পড়বে: প্রতিটা shaft একটা lift। খরচের rule idle lift বা caller এর দিকে ওই floor এর দিকে আসা lift এর জন্য শুধু দূরত্ব ধরে, আর উল্টো দিকে যাওয়া lift এর জন্য একটা penalty (building এর উচ্চতার দ্বিগুণ) যোগ করে। সবচেয়ে কম খরচ জেতে; সমান হলে প্রথম lift।

LOOK: একবারে একটা দিক

012345678928cargoing up: serve 8 firstnothing above: turn and serve 2stops: 8 then 2, one turn
Play চাপো। Floor 5 এ থাকা একটা lift এর থামার জায়গা 8 আর 2। সে আগে উপরে 8 পর্যন্ত যায়, তারপর ঘুরে 2 এ থামে। একবারই ঘোরা, এদিক-ওদিক না।

কীভাবে পড়বে: দুই দিকেই থামার জায়গা থাকলে 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 এর জন্য)

  1. কেন: "সবচেয়ে ভালো lift" এর rule building আর দিনের সময় অনুযায়ী বদলায়।
  2. কী সমাধান করে: dispatch হলো বদলানো যায় এমন একটা object, Dispatcher।
  3. না থাকলে: Call এর ভেতরে একটা if rushHour { … } বাড়তে থাকে।
  4. দাম: একটা interface; খরচ প্রায় নেই।
  5. কোথায়: 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

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 করে
TestNearestIdleCarWinsCall যায় সবচেয়ে কাছের idle lift এ
TestCarGoingTheWrongWayIsAvoidedদূরে চলে যাওয়া কাছের lift, দূরের idle lift এর কাছে হারে
TestLookServesStopsAheadFirst5 থেকে, থামার জায়গা 8 আর 2 হলে ক্রম হলো 8 তারপর 2
TestInvalidFloorRefusedFloor 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 / আগুন modeBuilding জুড়ে একটা state, যেটা সব lift কে নিচতলায় পাঠায় আর call উপেক্ষা করে।

Interview প্রশ্ন

Practice প্রশ্ন

Beginner

  1. কোন lift call নেবে, সেটা lift না, dispatcher কেন ঠিক করে?
  2. একটা lift এর state কী কী, আর কী তাকে একটা থেকে আরেকটায় নেয়?

Intermediate

  1. শুধু "সবচেয়ে কাছের lift" কেন যথেষ্ট না?
  2. Sleep না করে একটা চলমান system কীভাবে test করবে?

Advanced

  1. LOOK আর সবসময় সবচেয়ে কাছের থামার জায়গায় যাওয়া, তুলনা করো।
  2. 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 ট্র্যাক হবে।