Practice · Low Level Design · Model the Domain
Design a Parking Lot
সবচেয়ে বেশি আসা LLD warm-up: ভিন্ন size এর spot, ticket, বদলানো যায় এমন pricing strategy, আর code সহ পুরো class design।
Learn mode: পুরো solution নিচে দেখানো আছে।
Problem এর পরিচয়
| Difficulty | Beginner |
| সময় | Design আর code মিলিয়ে ১–২ ঘণ্টা; interview তে মোটামুটি ৪৫ মিনিট |
| আগে জানা দরকার | Class আর object, interface, enum, lock সম্পর্কে একটু ধারণা |
| যা শিখবে | Entity খুঁজে বের করা, encapsulation, composition, Strategy, clock inject করা, shared state এর চারপাশে mutex |
| বাস্তবে কোথায় | Shopping mall আর hospital এর parking, airport এর car park, gate এ "১২টা spot খালি" দেখানো app |
LLD interview তে সবচেয়ে বেশি আসা warm-up হলো parking lot, আর এর কারণ আছে। Parking lot কী জিনিস সেটা সবাই চেনে, তাই domain বোঝানো নিয়ে সময় যায় না। আসল প্রশ্ন হলো, একটা ঘোলা গল্প থেকে তুমি পরিষ্কার দায়িত্বওয়ালা কয়েকটা class বের করতে পারো কিনা। আর interviewer যখন জিজ্ঞেস করবে "pricing বদলাতে চাইলে?", তোমার design টা তখন টিকে থাকে কিনা।
Problem statement
ঢাকার কোনো shopping mall এর নিচের car park টার কথা ভাবো। কয়েকটা floor আছে। প্রতিটা floor এ ভিন্ন size এর spot: বাইকের জন্য চাপা জায়গা, গাড়ির জন্য সাধারণ, pickup আর ছোট truck এর জন্য চওড়া।
একটা গাড়ি gate এ আসলো। System কে তার জন্য একটা spot খুঁজে দিতে হবে, driver কে ticket দিতে হবে। পরে গাড়ি বের হওয়ার সময় যতক্ষণ ছিল সেই হিসাবে টাকা নিতে হবে, আর spot টা পরের গাড়ির জন্য খালি করে দিতে হবে।
আমরা কী বানাচ্ছি: gate machine এর ভেতরের software টা। Barrier এর motor না, camera না, শুধু সেই অংশ যেটা ঠিক করে কে কোথায় park করবে আর কত টাকা দেবে।
এটা কেন দরকার: এটা না থাকলে guard রা ঘুরে ঘুরে খালি spot খোঁজে, driver রা floor এর পর floor চক্কর দেয়, আর exit এ কত টাকা নেওয়া হবে সেটা নির্ভর করে counter এ কে বসে আছে তার উপর।
এক লাইনে কাজটা: একটা vehicle আসলে তাকে সবচেয়ে ভালো খালি spot আর একটা ticket দাও। একটা ticket আসলে ঠিক fee নাও আর spot খালি করো।
Requirement
Functional requirement
- Lot এ এক বা একাধিক level আছে। প্রতিটা level এ তিন size এর spot: small, medium আর large।
- তিন রকম vehicle: motorcycle (small লাগে), car (medium লাগে), truck (large লাগে)। দরকারের চেয়ে বড় spot এ রাখা যাবে, ছোটতে কখনো না।
- Park: vehicle এর জন্য একটা খালি spot খুঁজে ticket দাও, যেখানে থাকবে ID, spot আর ঢোকার সময়। আগে সবচেয়ে নিচের level দেখবে, আর সেই level এ যে spot এ আঁটে তার মধ্যে সবচেয়ে ছোটটা।
- Leave: ticket নিয়ে fee হিসাব করো, ticket টা paid করো, আর spot খালি করো।
- Pricing: প্রতিটা শুরু হওয়া ঘণ্টার জন্য টাকা, vehicle অনুযায়ী আলাদা rate, কমপক্ষে এক ঘণ্টা। Lot এর মালিক pricing এর নিয়ম বদলাতে পারবে (যেমন খেলার দিন এক দাম), কিন্তু parking এর code না ছুঁয়ে।
- খালি spot এর হিসাব: কোন size এর কয়টা spot খালি সেটা দেখাও, gate এর board এর জন্য।
- ভুল input ফেরত দাও: plate ছাড়া vehicle, যে plate আগে থেকেই ভেতরে, অচেনা ticket, আর যে ticket এর টাকা আগেই দেওয়া হয়ে গেছে।
Non-functional requirement
| Requirement | এখানে এর মানে |
|---|---|
| Concurrency তেও ঠিক থাকা | একটা lot এ কয়েকটা gate থাকে। একই মুহূর্তে আসা দুইটা গাড়ি কখনো একই spot পাবে না। |
| Extensibility | নতুন pricing, নতুন vehicle type, নতুন spot এর নিয়ম, এগুলো যোগ করতে হবে, পুরনো code আবার লিখতে হবে না। |
| Testability | Fee সময়ের উপর নির্ভর করে। Test এ "আড়াই ঘণ্টা পরে" check করতে হবে, sleep না করেই। |
| Performance | একটা mall এর lot এ কয়েকশো spot। প্রতিটা গাড়ির জন্য একবার পুরো list ঘুরে দেখলেও সমস্যা নেই। কখন এটা আর চলবে না সেটাও দেখাবো। |
| Maintainability | প্রতিটা class এর কাজ এক বাক্যে বলা যায়। |
যা ধরে নিচ্ছি আর যা এখানে নেই
- একটা building, সব memory তে। Database নেই, network নেই। ওটা এই problem এর HLD version।
- Payment সবসময় সফল হয়। Card, bKash আর cash সামলানো আলাদা একটা পুরো design এর ব্যাপার।
- Reservation, EV charging spot আর ticket হারানোর flow নেই। Design review তে এগুলো "যদি…" প্রশ্ন হিসেবে আবার আসবে।
যা জমা দিতে হবে
- Entity এর list, প্রতিটার কাজ এক বাক্যে।
- একটা class diagram।
ParkআরLeaveএর code, যেখানে pricing এর নিয়ম বদলানো যায়।- Fee এর নিয়ম আর শেষ spot এর জন্য দুই gate এর দৌড়, এই দুইটার test।
Hint 1: noun গুলো কোথায় নিয়ে যায়?
Requirement এর noun গুলোর নিচে দাগ দাও: lot, level, spot, vehicle, ticket, fee। বেশিরভাগই class হবে। শুধু "fee" টা আলাদা: এটা কোনো জিনিস না, এটা একটা নিয়ম। যে নিয়ম বদলায়, সেটা interface এর দিকে ইশারা করে।
Hint 2: গাড়ি কোথায় যাবে, কে ঠিক করবে?
Spot না, vehicle ও না। Spot ঠিক করতে গেলে প্রতিটা spot কে বাকি সব spot এর খবর রাখতে হতো। সিদ্ধান্তটা রাখো সেই object এ, যে সব level একসাথে দেখতে পায়।
Hint 3: "যেটায় আঁটে তার মধ্যে সবচেয়ে ছোট"
প্রতিটা level যদি তার spot গুলো ছোট থেকে বড় ক্রমে রাখে, তাহলে প্রথম যে খালি spot টা যথেষ্ট বড়, সেটাই best fit। Park করার সময় আর sort করতে হয় না।
Hint 4: সময় test করা
Lot এর ভেতরে গিয়ে সরাসরি time.Now() ডেকো না। Lot কে এমন একটা function দাও যেটা সময় বলে দেয়, আর test এ একটা নকল function পাঠাও।
মূল entity
Requirement পড়ে noun গুলো তুলে আনো। তারপর প্রতিটাকে জিজ্ঞেস করো: এর কি নিজের state আছে, কোনো behaviour আছে, আর এটা না থাকলে কি design খারাপ হতো?
| Entity | এক বাক্যে কাজ | কী রাখে | কী করে |
|---|---|---|---|
VehicleType | কী ধরনের vehicle সেটা বলে | একটা enum value | কোন size এর spot লাগবে সেটা বলে দেয় |
Vehicle | Gate এ যে দাঁড়িয়ে আছে | plate, type | কিছু না, শুধু একটা value |
SpotSize | ক্রম আছে এমন size, যাতে "বড়টায় ছোটটা আঁটে" এক comparison এ হয়ে যায় | একটা enum value | >= দিয়ে তুলনা |
Spot | Floor এ রং করা একটা চতুর্ভুজ | ID, level, size, কে আছে | খালি কিনা জানে |
Level | একটা floor | তার spot গুলো, ছোট থেকে বড় | যেটায় আঁটে এমন সবচেয়ে ভালো খালি spot খোঁজে |
Ticket | Driver এর রসিদ | ID, plate, spot, ঢোকা, বের হওয়া, fee, status | কিছু না, lot যে data ভরে দেয় |
PricingStrategy | যে নিয়মে থাকার সময় টাকায় বদলায় | নিয়ম অনুযায়ী | Fee(vehicle, stay) |
Lot | একমাত্র প্রবেশপথ, আর একমাত্র object যে সব দেখে | level, ticket, কে কোথায় আছে, clock, lock | Park, Leave, Free |
দুইটা জিনিস ইচ্ছা করেই class না:
- Gate. এই scope এ gate এর কাজ শুধু
Lot.ParkআরLot.Leaveডাকা। যে class শুধু call টা আরেকজনের কাছে পাঠিয়ে দেয়, সে একটা layer যোগ করে কিন্তু কোনো সিদ্ধান্ত যোগ করে না। পরে gate এর নিজের কাজ আসলে (plate পড়ার camera, যে printer আটকে যেতে পারে), তখন সে class হওয়ার যোগ্য। - Payment. এটা scope এর বাইরে, আর আনলে পুরো আরেকটা design টেনে আনতে হবে।
কে কার সাথে কীভাবে যুক্ত:
LotতারLevelগুলোর মালিক, আর প্রতিটাLevelতারSpotগুলোর মালিক। এটা composition: level ছাড়া spot এর কোনো অস্তিত্ব নেই, lot ছাড়া level এর ও না।LotTicketবানায় আর আসলটা নিজের কাছে রাখে। Driver সবসময় একটা copy পায়।Lotএকটা interface দিয়েPricingStrategyব্যবহার করে। পেছনে কোন নিয়ম আছে সেটা সে জানে না, জানার দরকারও নেই।Vehicleবাইরে থেকে আসে, object হিসেবে কোথাও রাখা হয় না। Lot শুধু plate আর type রাখে, ওটুকুই তার লাগে।
Diagram
Class diagram
কীভাবে পড়বে: প্রতিটা box একটা class: উপরে নাম, তারপর field, তারপর method। বাম দিকে মালিকানা: একটা Lot এর অনেক Level, প্রতিটা Level এর অনেক Spot। Lot যত Ticket দেয়, সবগুলো নিজের কাছেও রাখে। ডান দিকে Lot এর কাছে একটা PricingStrategy, যেটা একটা interface; HourlyPricing আর FlatPricing তার দুইটা উত্তর। এই একটা তীরের জন্যই খেলার দিন Lot এ হাত না দিয়ে flat ভাড়ায় চলে যাওয়া যায়।
চিহ্ন গুলো: + মানে public, - মানে private। ভরা diamond ◆ মানে মালিক (অংশটা মালিকের সাথেই আসে, সাথেই যায়), সাধারণ তীর মানে reference ধরে রাখে, dashed তীর মানে ব্যবহার করে, আর ফাঁপা ত্রিভুজওয়ালা dashed তীর মানে এই interface implement করে।
Sequence: একটা গাড়ি park করছে
কীভাবে পড়বে: প্রতিটা খাড়া dashed লাইন একটা object, আর সময় চলে উপর থেকে নিচে। ভরা তীর মানে call, dashed তীর মানে উত্তর ফেরত আসা, আর সরু bar গুলো দেখায় কোন object কখন কাজে ব্যস্ত। CAR-C Lot এর Park ডাকে। Lot একে একে প্রতিটা level কে জিজ্ঞেস করে। Level 1 বলে "nil": তার medium spot গুলো ভরা, large টাও। Level 2 তে S1 খালি। Lot spot টা দখল দেখায়, ticket T0005 বানায় আর একটা copy ফেরত দেয়।
Fail করলে কী হয়: সব level nil বললে lot ErrLotFull ফেরত দেয়, কোনো ticket তৈরি হয় না। কিছুই অর্ধেক হয়ে থাকে না, কারণ spot পাওয়ার পরেই শুধু সেটাকে দখল দেখানো হয়।
State: একটা ticket এর জীবন
কীভাবে পড়বে: ticket এর state মাত্র দুইটা। Park এটাকে Active হিসেবে বানায় (spot দখল)। Leave এটাকে Paid করে, fee বসায় আর spot খালি করে। ফেরার কোনো তীর নেই। Paid ticket এ আবার Leave করলে আসে ErrTicketPaid। এটাই আটকায় যাতে একটা buggy exit gate কারো spot দুইবার খালি করে না দেয়।
Concurrency: দুইটা gate, একটা spot
কীভাবে পড়বে: শেষ খালি spot এর জন্য দুইটা gate একই মুহূর্তে Park ডাকে। Gate A আগে lock পায়, L1-S1 নেয়, lock ছেড়ে দেয়। Gate B এতক্ষণ অপেক্ষা করছিল। সে যখন ঢোকে, তখন আর কোনো spot নেই, তাই পায় "lot full"। Lock না থাকলে দুজনেই "L1-S1 খালি" পড়ে ফেলতে পারতো কেউ "দখল" লেখার আগেই, আর দুইটা গাড়ি পাঠানো হতো একটা চতুর্ভুজে। Testing section এর test টা দেখায় এটা সত্যিই ঘটে।
Requirement বাড়লে: একটা lock থাকায় পুরো Park call এর জন্য gate গুলো পালা করে ঢোকে। কয়েকশো spot এ এটা microsecond এর ব্যাপার, কেউ টেরও পায় না। বিশাল lot হলে পুরো lot এর জায়গায় প্রতিটা level এ আলাদা lock দেবে। Review section এ এটা আছে।
Design principle
শুধু সেই principle গুলো, যেগুলো সত্যিই এই design কে আকার দিয়েছে:
- Single responsibility.
Spotজানে সে খালি কিনা।Levelএকটা floor এ spot খোঁজে।Lotসবকিছু সমন্বয় করে।PricingStrategyদাম বলে। Pricing এর নিয়ম বদলালে ঠিক একটা type বদলায়। - Encapsulation. Spot এর
platefield টা private (Go তে ছোট হাতের অক্ষর)। Package এর বাইরের কেউLot.Parkছাড়া spot দখল দেখাতে পারে না, তাই lock এড়িয়ে যাওয়ার রাস্তা নেই।ParkআরLeaveticket এর copy ফেরত দেয়। কেউ নিজের copy তেStatus = Paidলিখলে আসলটায় কিছুই বদলায় না। - Open/closed. Lot পরিবর্তনের জন্য বন্ধ, কিন্তু নতুন pricing এর জন্য খোলা:
Feemethod ওয়ালা একটা type লিখে পাঠিয়ে দিলেই হলো। - Dependency inversion.
Lotনির্ভর করেPricingStrategyinterface আর একটাnow func() time.Timeএর উপর, কোনো নির্দিষ্ট pricing type বা system clock এর উপর না। এজন্যই এটা test করা যায়। - Composition over inheritance.
SmallSpot,MediumSpot,LargeSpotsubclass ওয়ালা কোনোSpotbase class নেই। Size গুলোর পার্থক্য শুধু data তে, behaviour এ না, তাই একটাSizefield ই যথেষ্ট। এখানে subclass বানালে তিনটা class বাড়তো, সিদ্ধান্ত বাড়তো শূন্যটা। - KISS.
Gateনেই,Floorinterface নেই, spot খোঁজার আলাদা strategy নেই। প্রতিটাই পরে নেওয়ার মতো যুক্তিসঙ্গত ধাপ, কিন্তু এখনকার requirement এ কোনোটাই লাগে না।
Design pattern
Strategy: pricing এর জন্য
- কেন দরকার: মালিক fee এর হিসাব বদলাতে চায় (ঘণ্টা ধরে, এক দামে, রাতের rate, ছুটির দিনের rate), parking এর logic না ছুঁয়ে।
- কী সমাধান করে: "কত টাকা" প্রশ্নটা
Lotথেকে বের করে একটা method ওয়ালা interface এর পেছনে রাখে:Fee(vehicle, stay)। - না থাকলে কী হতো:
Leaveএর ভেতরেif isMatchDay { ... } else if isNight { ... }এর chain বাড়তেই থাকতো। প্রতিটা নতুন নিয়মেLotedit করতে হতো, মানে দাম বদলাতে গিয়ে parking ভেঙে ফেলার ঝুঁকি। - দাম কী: একটা type বেশি, আর একটা ধাপ ঘুরে call। Pricing যদি সত্যিই চিরকাল একই থাকতো, একটা সাধারণ function ই চলতো। কিন্তু requirement বলছে "parking এর code না বদলে বদলাতে হবে", তাই interface টা নিজের খরচ তুলে নেয়।
- কোথায় আছে:
pricing.goতেPricingStrategy,HourlyPricingআরFlatPricing।Lot.Leaveডাকেl.pricing.Fee(...)।TestSwapPricingtest টাFlatPricingদিয়ে lot বানায়, আর কিছুই বদলাতে হয় না।
Functional options: clock এর জন্য
NewLot(levels, pricing, WithClock(fake)) হলো Go তে optional setting দেওয়ার চলতি উপায়, আট parameter ওয়ালা constructor ছাড়াই। Java তে এর জায়গায় builder বা overloaded constructor, Python এ default সহ একটা keyword argument। Pattern এর নামের চেয়ে মূল idea টা বেশি জরুরি: সময়ও একটা dependency, তাই সেটা inject করো।
যে pattern গুলো নেইনি, আর কেন
- Lot এর জন্য Singleton. লোভ লাগে ("lot তো একটাই"), কিন্তু এতে test গুলো state ভাগাভাগি করে, আর দ্বিতীয় building আনা অসম্ভব হয়ে যায়।
mainএ একটাLotবানিয়ে সেটাই পাঠাতে থাকো। - Vehicle এর জন্য Factory. Vehicle মানে দুইটা field।
Vehicle{Plate: "A", Type: Car}এর চেয়ে পরিষ্কার আর হয় না। - Ticket এর জন্য State pattern. দুইটা state আর একটা transition। একটা enum আর একটা
ifদিয়েই কথাটা সোজাভাবে বলা যায়। State pattern এর আসল জায়গা হলো Vending Machine।
Go তে পুরো solution
Project structure
parking/
├── go.mod
├── vehicle.go VehicleType, Vehicle
├── spot.go SpotSize, Spot, sizeFor
├── level.go Level and best-fit search
├── ticket.go Ticket, TicketStatus
├── pricing.go PricingStrategy, HourlyPricing, FlatPricing
├── lot.go Lot: Park, Leave, Free, the lock and the errors
├── lot_test.go tests, including the concurrency test
└── cmd/demo/
└── main.go a morning at the lot
ধাপ ১: vehicle আর size
শব্দভাণ্ডার দিয়ে শুরু। SpotSize একটা ক্রমওয়ালা enum, তাই "এই spot এ আঁটবে?" প্রশ্নটা শুধু spot.Size >= need।
// vehicle.go
package parking
// VehicleType is what drives in. It decides which spots fit and what the hour costs.
type VehicleType int
const (
Motorcycle VehicleType = iota
Car
Truck
)
func (v VehicleType) String() string {
return [...]string{"motorcycle", "car", "truck"}[v]
}
// Vehicle is a value: two vehicles with the same plate and type are the same vehicle.
type Vehicle struct {
Plate string
Type VehicleType
}
// spot.go
package parking
// SpotSize is ordered: a bigger spot can always take a smaller vehicle.
type SpotSize int
const (
Small SpotSize = iota
Medium
Large
)
func (s SpotSize) String() string {
return [...]string{"small", "medium", "large"}[s]
}
// sizeFor is the smallest spot a vehicle type fits in.
func sizeFor(t VehicleType) SpotSize {
switch t {
case Motorcycle:
return Small
case Car:
return Medium
default:
return Large
}
}
// Spot knows only itself: its size and who is in it. The lot decides who goes where.
type Spot struct {
ID string
Level int
Size SpotSize
plate string // "" means free
}
func (s *Spot) free() bool { return s.plate == "" }
ধাপ ২: যে level সবচেয়ে ভালো spot খুঁজে দেয়
কৌশলটা constructor এ: spot গুলো যোগ হয় ছোট থেকে বড় ক্রমে। তাই প্রথম যে খালি spot যথেষ্ট বড়, সেটাই আঁটে এমন সবচেয়ে ছোট spot। Park এর সময় sort নেই, "best fit" এর আলাদা logic ও নেই।
// level.go
package parking
import "fmt"
// Level is one floor. Its spots are kept smallest first, so the first free spot that
// fits is also the best fit: a motorcycle never takes a truck's spot while a small one
// is free.
type Level struct {
Number int
spots []*Spot
}
func NewLevel(number, small, medium, large int) *Level {
l := &Level{Number: number}
n := 0
add := func(count int, size SpotSize) {
for range count {
n++
l.spots = append(l.spots, &Spot{ID: fmt.Sprintf("L%d-S%d", number, n), Level: number, Size: size})
}
}
add(small, Small)
add(medium, Medium)
add(large, Large)
return l
}
func (l *Level) findSpot(need SpotSize) *Spot {
for _, s := range l.spots {
if s.free() && s.Size >= need {
return s
}
}
return nil
}
ধাপ ৩: ticket আর pricing এর নিয়ম
// ticket.go
package parking
import "time"
type TicketStatus int
const (
Active TicketStatus = iota // car is inside
Paid // car has left; the ticket is closed for good
)
// Ticket is the receipt the driver holds. Callers get copies, so nobody outside the lot
// can flip a ticket to Paid without going through Leave.
type Ticket struct {
ID string
Plate string
Vehicle VehicleType
SpotID string
Entry time.Time
Exit time.Time
Fee int // taka
Status TicketStatus
}
HourlyPricing integer হিসাব দিয়ে উপরের দিকে round করে। ভাগের আগে time.Hour - 1 যোগ করলে "১ ঘণ্টা ১ মিনিট" হয়ে যায় ২, আর ঠিক ১ ঘণ্টা থাকে ১। Float দিয়েও হতো, কিন্তু integer এর হিসাব হঠাৎ 0.9999 দিয়ে চমকে দেয় না।
// pricing.go
package parking
import "time"
// PricingStrategy turns a stay into money. The lot doesn't know how; it just asks.
type PricingStrategy interface {
Fee(v VehicleType, stay time.Duration) int
}
// HourlyPricing charges per started hour, at least one hour.
type HourlyPricing struct {
Rates map[VehicleType]int // taka per hour
}
func (p HourlyPricing) Fee(v VehicleType, stay time.Duration) int {
hours := int((stay + time.Hour - 1) / time.Hour) // round up
if hours < 1 {
hours = 1
}
return hours * p.Rates[v]
}
// FlatPricing charges one amount per visit, e.g. on a match day.
type FlatPricing struct {
Amount int
}
func (p FlatPricing) Fee(VehicleType, time.Duration) int { return p.Amount }
ধাপ ৪: lot, যেখানে সব সিদ্ধান্ত হয়
যা কিছু বদলায় (কোন spot এ কোন plate, কোন কোন ticket আছে), সেগুলো ছোঁয়া হয় শুধু mu ধরে থাকা অবস্থায়। Park আগে plate check করে, তারপর ক্রম ধরে level গুলো দেখে। তাই "সবচেয়ে নিচের level আগে" নিয়মটা loop থেকেই এমনি এসে যায়।
// lot.go
package parking
import (
"errors"
"fmt"
"sync"
"time"
)
var (
ErrInvalidVehicle = errors.New("vehicle needs a plate")
ErrLotFull = errors.New("no free spot for this vehicle")
ErrAlreadyParked = errors.New("vehicle is already parked")
ErrUnknownTicket = errors.New("unknown ticket")
ErrTicketPaid = errors.New("ticket already paid")
)
// Lot is the one entry point: Park, Leave, Free. One mutex guards every spot and ticket,
// so two gates can never hand out the same spot.
type Lot struct {
mu sync.Mutex
levels []*Level
pricing PricingStrategy
now func() time.Time
spots map[string]*Spot // spot ID → spot
tickets map[string]*Ticket // ticket ID → ticket
parked map[string]*Ticket // plate → active ticket
nextID int
}
type Option func(*Lot)
// WithClock swaps the clock, so tests can say "two and a half hours later" instantly.
func WithClock(now func() time.Time) Option {
return func(l *Lot) { l.now = now }
}
func NewLot(levels []*Level, pricing PricingStrategy, opts ...Option) *Lot {
l := &Lot{
levels: levels,
pricing: pricing,
now: time.Now,
spots: map[string]*Spot{},
tickets: map[string]*Ticket{},
parked: map[string]*Ticket{},
}
for _, lv := range levels {
for _, s := range lv.spots {
l.spots[s.ID] = s
}
}
for _, o := range opts {
o(l)
}
return l
}
// Park finds the best spot on the lowest level that has one and issues a ticket.
func (l *Lot) Park(v Vehicle) (Ticket, error) {
if v.Plate == "" {
return Ticket{}, ErrInvalidVehicle
}
l.mu.Lock()
defer l.mu.Unlock()
if _, ok := l.parked[v.Plate]; ok {
return Ticket{}, ErrAlreadyParked
}
need := sizeFor(v.Type)
for _, lv := range l.levels {
s := lv.findSpot(need)
if s == nil {
continue
}
s.plate = v.Plate
l.nextID++
t := &Ticket{
ID: fmt.Sprintf("T%04d", l.nextID),
Plate: v.Plate,
Vehicle: v.Type,
SpotID: s.ID,
Entry: l.now(),
Status: Active,
}
l.tickets[t.ID] = t
l.parked[v.Plate] = t
return *t, nil
}
return Ticket{}, ErrLotFull
}
// Leave closes the ticket, charges for the stay and frees the spot.
func (l *Lot) Leave(ticketID string) (Ticket, error) {
l.mu.Lock()
defer l.mu.Unlock()
t, ok := l.tickets[ticketID]
if !ok {
return Ticket{}, ErrUnknownTicket
}
if t.Status == Paid {
return Ticket{}, ErrTicketPaid
}
t.Exit = l.now()
t.Fee = l.pricing.Fee(t.Vehicle, t.Exit.Sub(t.Entry))
t.Status = Paid
l.spots[t.SpotID].plate = ""
delete(l.parked, t.Plate)
return *t, nil
}
// Free counts free spots by size, for the board at the gate.
func (l *Lot) Free() map[SpotSize]int {
l.mu.Lock()
defer l.mu.Unlock()
free := map[SpotSize]int{}
for _, s := range l.spots {
if s.free() {
free[s.Size]++
}
}
return free
}
ধাপ ৫: চালিয়ে দেখো
Demo টা দুই level এ ৬টা spot নিয়ে একটা lot খোলে আর ৭টা vehicle ঢোকায়। Clock একটা variable, তাই "আড়াই ঘণ্টা পরে" মানে এক লাইন।
// cmd/demo/main.go
package main
import (
"fmt"
"time"
"parking"
)
func main() {
clock := time.Date(2026, 11, 7, 9, 0, 0, 0, time.UTC)
lot := parking.NewLot(
[]*parking.Level{
parking.NewLevel(1, 1, 2, 1), // 1 small, 2 medium, 1 large
parking.NewLevel(2, 0, 1, 1),
},
parking.HourlyPricing{Rates: map[parking.VehicleType]int{
parking.Motorcycle: 20, parking.Car: 50, parking.Truck: 100,
}},
parking.WithClock(func() time.Time { return clock }),
)
arrivals := []parking.Vehicle{
{Plate: "BIKE-1", Type: parking.Motorcycle},
{Plate: "CAR-A", Type: parking.Car},
{Plate: "CAR-B", Type: parking.Car},
{Plate: "TRUCK-1", Type: parking.Truck},
{Plate: "CAR-C", Type: parking.Car},
{Plate: "CAR-D", Type: parking.Car},
{Plate: "TRUCK-2", Type: parking.Truck},
}
tickets := map[string]string{}
for _, v := range arrivals {
t, err := lot.Park(v)
if err != nil {
fmt.Printf("%-8s %-10s -> %v\n", v.Plate, v.Type, err)
continue
}
tickets[v.Plate] = t.ID
fmt.Printf("%-8s %-10s -> %s (ticket %s)\n", v.Plate, v.Type, t.SpotID, t.ID)
}
clock = clock.Add(2*time.Hour + 30*time.Minute)
t, _ := lot.Leave(tickets["CAR-A"])
fmt.Printf("CAR-A left after %v, fee %d taka\n", t.Exit.Sub(t.Entry), t.Fee)
if _, err := lot.Leave(tickets["CAR-A"]); err != nil {
fmt.Println("CAR-A again:", err)
}
// A medium spot is free now, but a truck needs a large one, and CAR-D is in it.
if _, err := lot.Park(parking.Vehicle{Plate: "TRUCK-2", Type: parking.Truck}); err != nil {
fmt.Println("TRUCK-2 retry ->", err)
}
free := lot.Free()
fmt.Printf("free: small=%d medium=%d large=%d\n", free[parking.Small], free[parking.Medium], free[parking.Large])
}
$ go run ./cmd/demo
BIKE-1 motorcycle -> L1-S1 (ticket T0001)
CAR-A car -> L1-S2 (ticket T0002)
CAR-B car -> L1-S3 (ticket T0003)
TRUCK-1 truck -> L1-S4 (ticket T0004)
CAR-C car -> L2-S1 (ticket T0005)
CAR-D car -> L2-S2 (ticket T0006)
TRUCK-2 truck -> no free spot for this vehicle
CAR-A left after 2h30m0s, fee 150 taka
CAR-A again: ticket already paid
TRUCK-2 retry -> no free spot for this vehicle
free: small=0 medium=1 large=0
Output টা পড়ি:
BIKE-1নেয় small spotL1-S1, যেটা ওর জন্যই বানানো।CAR-AআরCAR-BLevel 1 এর medium spot দুইটা ভরে ফেলে।TRUCK-1নেয় large টা।CAR-Cযায় Level 2 তে, কারণ Level 1 এ গাড়ির জন্য আর কিছু খালি নেই।CAR-Dদেখে Level 2 এর medium spot ও দখল, তাই সে নেয় large spotL2-S2। এটা নিয়মের মধ্যেই: truck এর spot এ গাড়ি আঁটে।TRUCK-2কে ফিরিয়ে দেওয়া হয়: কোথাও আর large spot নেই।CAR-Aআড়াই ঘণ্টা পরে বের হয় আর ৩টা শুরু হওয়া ঘণ্টার টাকা দেয়: ৩ × ৫০ = ১৫০ টাকা। একই ticket এ দ্বিতীয়বারLeaveফিরিয়ে দেওয়া হয়।TRUCK-2আবার চেষ্টা করে। এখন একটা medium spot খালি, কিন্তু truck এর লাগে large, আর সেখানে বসে আছেCAR-D। এই লাইনটা মনে রাখো, design review তে এটাই এই problem এর সবচেয়ে ভালো "যদি…" প্রশ্ন।
বাকি পাঁচটা language
একই design, একই demo, একই output, শুধু প্রতিটা language তার নিজের ধরনে লেখা। নিচের প্রতিটা program চালানো হয়েছে, আর output Go এর সাথে লাইন ধরে মিলেছে।
from __future__ import annotations
import threading
from dataclasses import dataclass, replace
from datetime import datetime, timedelta
from enum import IntEnum
from typing import Callable, Protocol
class VehicleType(IntEnum):
MOTORCYCLE = 0
CAR = 1
TRUCK = 2
class SpotSize(IntEnum): # ordered: a bigger spot takes a smaller vehicle
SMALL = 0
MEDIUM = 1
LARGE = 2
SIZE_FOR = {VehicleType.MOTORCYCLE: SpotSize.SMALL, VehicleType.CAR: SpotSize.MEDIUM,
VehicleType.TRUCK: SpotSize.LARGE}
@dataclass(frozen=True)
class Vehicle:
plate: str
type: VehicleType
@dataclass
class Spot:
id: str
size: SpotSize
plate: str = "" # "" means free
@dataclass(frozen=True) # frozen: callers can't flip a ticket to paid
class Ticket:
id: str
plate: str
vehicle: VehicleType
spot_id: str
entry: datetime
exit: datetime | None = None
fee: int = 0
paid: bool = False
class LotFull(Exception): ...
class AlreadyParked(Exception): ...
class UnknownTicket(Exception): ...
class TicketPaid(Exception): ...
class PricingStrategy(Protocol):
def fee(self, v: VehicleType, stay: timedelta) -> int: ...
class HourlyPricing:
def __init__(self, rates: dict[VehicleType, int]):
self.rates = rates
def fee(self, v: VehicleType, stay: timedelta) -> int:
hours = max(1, -(-int(stay.total_seconds()) // 3600)) # round up, min 1
return hours * self.rates[v]
class Level:
def __init__(self, number: int, small: int, medium: int, large: int):
sizes = [SpotSize.SMALL] * small + [SpotSize.MEDIUM] * medium + [SpotSize.LARGE] * large
self.spots = [Spot(f"L{number}-S{i + 1}", s) for i, s in enumerate(sizes)]
def find_spot(self, need: SpotSize) -> Spot | None:
return next((s for s in self.spots if not s.plate and s.size >= need), None)
class Lot:
def __init__(self, levels: list[Level], pricing: PricingStrategy,
now: Callable[[], datetime] = datetime.now):
self._lock = threading.Lock()
self._levels, self._pricing, self._now = levels, pricing, now
self._spots = {s.id: s for lv in levels for s in lv.spots}
self._tickets: dict[str, Ticket] = {}
self._parked: dict[str, str] = {} # plate -> ticket id
self._next = 0
def park(self, v: Vehicle) -> Ticket:
if not v.plate:
raise ValueError("vehicle needs a plate")
with self._lock:
if v.plate in self._parked:
raise AlreadyParked("vehicle is already parked")
for lv in self._levels:
spot = lv.find_spot(SIZE_FOR[v.type])
if spot:
spot.plate = v.plate
self._next += 1
t = Ticket(f"T{self._next:04d}", v.plate, v.type, spot.id, self._now())
self._tickets[t.id] = t
self._parked[v.plate] = t.id
return t
raise LotFull("no free spot for this vehicle")
def leave(self, ticket_id: str) -> Ticket:
with self._lock:
t = self._tickets.get(ticket_id)
if t is None:
raise UnknownTicket("unknown ticket")
if t.paid:
raise TicketPaid("ticket already paid")
exit_at = self._now()
t = replace(t, exit=exit_at, fee=self._pricing.fee(t.vehicle, exit_at - t.entry), paid=True)
self._tickets[t.id] = t
self._spots[t.spot_id].plate = ""
del self._parked[t.plate]
return t
def free(self) -> dict[SpotSize, int]:
with self._lock:
out = {s: 0 for s in SpotSize}
for s in self._spots.values():
if not s.plate:
out[s.size] += 1
return out
if __name__ == "__main__":
clock = [datetime(2026, 11, 7, 9, 0)]
lot = Lot([Level(1, 1, 2, 1), Level(2, 0, 1, 1)],
HourlyPricing({VehicleType.MOTORCYCLE: 20, VehicleType.CAR: 50, VehicleType.TRUCK: 100}),
now=lambda: clock[0])
M, C, T = VehicleType.MOTORCYCLE, VehicleType.CAR, VehicleType.TRUCK
tickets = {}
for v in [Vehicle("BIKE-1", M), Vehicle("CAR-A", C), Vehicle("CAR-B", C), Vehicle("TRUCK-1", T),
Vehicle("CAR-C", C), Vehicle("CAR-D", C), Vehicle("TRUCK-2", T)]:
name = v.type.name.lower()
try:
t = lot.park(v)
tickets[v.plate] = t.id
print(f"{v.plate:<8} {name:<10} -> {t.spot_id} (ticket {t.id})")
except LotFull as e:
print(f"{v.plate:<8} {name:<10} -> {e}")
clock[0] += timedelta(hours=2, minutes=30)
t = lot.leave(tickets["CAR-A"])
h, m = divmod(int((t.exit - t.entry).total_seconds()) // 60, 60)
print(f"CAR-A left after {h}h{m}m0s, fee {t.fee} taka")
try:
lot.leave(tickets["CAR-A"])
except TicketPaid as e:
print("CAR-A again:", e)
try:
lot.park(Vehicle("TRUCK-2", T))
except LotFull as e:
print("TRUCK-2 retry ->", e)
f = lot.free()
print(f"free: small={f[SpotSize.SMALL]} medium={f[SpotSize.MEDIUM]} large={f[SpotSize.LARGE]}")// Ordered sizes: a bigger spot takes a smaller vehicle.
const SIZES = ["small", "medium", "large"] as const;
type SpotSize = 0 | 1 | 2;
type VehicleType = "motorcycle" | "car" | "truck";
const SIZE_FOR: Record<VehicleType, SpotSize> = { motorcycle: 0, car: 1, truck: 2 };
interface Vehicle { readonly plate: string; readonly type: VehicleType }
interface Spot { readonly id: string; readonly size: SpotSize; plate: string } // "" = free
// Readonly: callers get a ticket they can't mark as paid themselves.
interface Ticket {
readonly id: string; readonly plate: string; readonly vehicle: VehicleType;
readonly spotId: string; readonly entry: Date;
readonly exit?: Date; readonly fee: number; readonly paid: boolean;
}
interface PricingStrategy { fee(v: VehicleType, stayMs: number): number }
class HourlyPricing implements PricingStrategy {
private readonly rates: Record<VehicleType, number>;
constructor(rates: Record<VehicleType, number>) { this.rates = rates; }
fee(v: VehicleType, stayMs: number): number {
const hours = Math.max(1, Math.ceil(stayMs / 3_600_000));
return hours * this.rates[v];
}
}
class ParkingError extends Error {}
class Level {
readonly spots: Spot[] = [];
constructor(n: number, small: number, medium: number, large: number) {
const sizes: SpotSize[] = [...Array(small).fill(0), ...Array(medium).fill(1), ...Array(large).fill(2)];
sizes.forEach((size, i) => this.spots.push({ id: `L${n}-S${i + 1}`, size, plate: "" }));
}
findSpot(need: SpotSize): Spot | undefined {
return this.spots.find((s) => !s.plate && s.size >= need);
}
}
// No lock: JavaScript runs this code on one thread, and park() has no await inside,
// so two gates can't interleave in the middle of it.
class Lot {
private readonly spots = new Map<string, Spot>();
private readonly tickets = new Map<string, Ticket>();
private readonly parked = new Map<string, string>(); // plate -> ticket id
private next = 0;
private readonly levels: Level[];
private readonly pricing: PricingStrategy;
private readonly now: () => Date;
constructor(levels: Level[], pricing: PricingStrategy, now: () => Date = () => new Date()) {
this.levels = levels; this.pricing = pricing; this.now = now;
for (const lv of levels) for (const s of lv.spots) this.spots.set(s.id, s);
}
park(v: Vehicle): Ticket {
if (!v.plate) throw new ParkingError("vehicle needs a plate");
if (this.parked.has(v.plate)) throw new ParkingError("vehicle is already parked");
for (const lv of this.levels) {
const spot = lv.findSpot(SIZE_FOR[v.type]);
if (!spot) continue;
spot.plate = v.plate;
const t: Ticket = {
id: `T${String(++this.next).padStart(4, "0")}`, plate: v.plate, vehicle: v.type,
spotId: spot.id, entry: this.now(), fee: 0, paid: false,
};
this.tickets.set(t.id, t);
this.parked.set(v.plate, t.id);
return t;
}
throw new ParkingError("no free spot for this vehicle");
}
leave(ticketId: string): Ticket {
const t = this.tickets.get(ticketId);
if (!t) throw new ParkingError("unknown ticket");
if (t.paid) throw new ParkingError("ticket already paid");
const exit = this.now();
const done: Ticket = { ...t, exit, fee: this.pricing.fee(t.vehicle, exit.getTime() - t.entry.getTime()), paid: true };
this.tickets.set(t.id, done);
this.spots.get(t.spotId)!.plate = "";
this.parked.delete(t.plate);
return done;
}
free(): number[] {
const out = [0, 0, 0];
for (const s of this.spots.values()) if (!s.plate) out[s.size]++;
return out;
}
}
let clock = new Date(Date.UTC(2026, 10, 7, 9, 0));
const lot = new Lot(
[new Level(1, 1, 2, 1), new Level(2, 0, 1, 1)],
new HourlyPricing({ motorcycle: 20, car: 50, truck: 100 }),
() => clock,
);
const tickets = new Map<string, string>();
const arrivals: Vehicle[] = [
{ plate: "BIKE-1", type: "motorcycle" }, { plate: "CAR-A", type: "car" }, { plate: "CAR-B", type: "car" },
{ plate: "TRUCK-1", type: "truck" }, { plate: "CAR-C", type: "car" }, { plate: "CAR-D", type: "car" },
{ plate: "TRUCK-2", type: "truck" },
];
for (const v of arrivals) {
try {
const t = lot.park(v);
tickets.set(v.plate, t.id);
console.log(`${v.plate.padEnd(8)} ${v.type.padEnd(10)} -> ${t.spotId} (ticket ${t.id})`);
} catch (e) {
console.log(`${v.plate.padEnd(8)} ${v.type.padEnd(10)} -> ${(e as Error).message}`);
}
}
clock = new Date(clock.getTime() + 150 * 60_000);
const out = lot.leave(tickets.get("CAR-A")!);
const mins = (out.exit!.getTime() - out.entry.getTime()) / 60_000;
console.log(`CAR-A left after ${Math.floor(mins / 60)}h${mins % 60}m0s, fee ${out.fee} taka`);
try { lot.leave(tickets.get("CAR-A")!); } catch (e) { console.log("CAR-A again:", (e as Error).message); }
try { lot.park({ plate: "TRUCK-2", type: "truck" }); } catch (e) { console.log("TRUCK-2 retry ->", (e as Error).message); }
const f = lot.free();
console.log(`free: ${SIZES.map((s, i) => `${s}=${f[i]}`).join(" ")}`);use std::collections::HashMap;
use std::fmt;
use std::sync::Mutex;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum VehicleType { Motorcycle, Car, Truck }
// Ordered: derive(PartialOrd) compares by declaration order, so Large > Small.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
enum SpotSize { Small, Medium, Large }
impl VehicleType {
fn needs(self) -> SpotSize {
match self { Self::Motorcycle => SpotSize::Small, Self::Car => SpotSize::Medium, Self::Truck => SpotSize::Large }
}
}
impl fmt::Display for VehicleType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self { Self::Motorcycle => "motorcycle", Self::Car => "car", Self::Truck => "truck" };
f.pad(s) // pad() honours {:<10}
}
}
struct Vehicle { plate: String, kind: VehicleType }
struct Spot { id: String, size: SpotSize, plate: Option<String> }
#[derive(Clone, Debug)] // callers get a clone, never a reference into the lot
struct Ticket { id: String, plate: String, vehicle: VehicleType, spot_id: String, entry: u64, exit: u64, fee: u32, paid: bool }
#[derive(Debug, PartialEq)]
enum ParkError { InvalidVehicle, LotFull, AlreadyParked, UnknownTicket, TicketPaid }
impl fmt::Display for ParkError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self {
Self::InvalidVehicle => "vehicle needs a plate",
Self::LotFull => "no free spot for this vehicle",
Self::AlreadyParked => "vehicle is already parked",
Self::UnknownTicket => "unknown ticket",
Self::TicketPaid => "ticket already paid",
})
}
}
// Send + Sync so the lot can sit behind an Arc and be shared by gate threads.
trait PricingStrategy: Send + Sync { fn fee(&self, v: VehicleType, stay_secs: u64) -> u32; }
struct HourlyPricing { rates: HashMap<VehicleType, u32> }
impl PricingStrategy for HourlyPricing {
fn fee(&self, v: VehicleType, stay_secs: u64) -> u32 {
let hours = stay_secs.div_ceil(3600).max(1) as u32;
hours * self.rates[&v]
}
}
fn level(n: u32, small: usize, medium: usize, large: usize) -> Vec<Spot> {
let sizes = [(small, SpotSize::Small), (medium, SpotSize::Medium), (large, SpotSize::Large)];
sizes.iter().flat_map(|&(c, s)| std::iter::repeat_n(s, c)).enumerate()
.map(|(i, size)| Spot { id: format!("L{n}-S{}", i + 1), size, plate: None }).collect()
}
// Everything that changes lives inside the Mutex, so the compiler won't let you touch
// a spot without holding the lock.
struct State { levels: Vec<Vec<Spot>>, tickets: HashMap<String, Ticket>, parked: HashMap<String, String>, next: u32 }
struct Lot { state: Mutex<State>, pricing: Box<dyn PricingStrategy>, now: Box<dyn Fn() -> u64 + Send + Sync> }
impl Lot {
fn new(levels: Vec<Vec<Spot>>, pricing: Box<dyn PricingStrategy>, now: Box<dyn Fn() -> u64 + Send + Sync>) -> Self {
Lot { state: Mutex::new(State { levels, tickets: HashMap::new(), parked: HashMap::new(), next: 0 }), pricing, now }
}
fn park(&self, v: &Vehicle) -> Result<Ticket, ParkError> {
if v.plate.is_empty() { return Err(ParkError::InvalidVehicle); }
let mut st = self.state.lock().unwrap();
if st.parked.contains_key(&v.plate) { return Err(ParkError::AlreadyParked); }
let need = v.kind.needs();
let spot = st.levels.iter_mut().flatten()
.find(|s| s.plate.is_none() && s.size >= need)
.ok_or(ParkError::LotFull)?;
spot.plate = Some(v.plate.clone());
let spot_id = spot.id.clone();
st.next += 1;
let t = Ticket { id: format!("T{:04}", st.next), plate: v.plate.clone(), vehicle: v.kind, spot_id, entry: (self.now)(), exit: 0, fee: 0, paid: false };
st.tickets.insert(t.id.clone(), t.clone());
st.parked.insert(v.plate.clone(), t.id.clone());
Ok(t)
}
fn leave(&self, ticket_id: &str) -> Result<Ticket, ParkError> {
let mut st = self.state.lock().unwrap();
let now = (self.now)();
let t = st.tickets.get_mut(ticket_id).ok_or(ParkError::UnknownTicket)?;
if t.paid { return Err(ParkError::TicketPaid); }
t.exit = now;
t.fee = self.pricing.fee(t.vehicle, t.exit - t.entry);
t.paid = true;
let done = t.clone();
for s in st.levels.iter_mut().flatten() { if s.id == done.spot_id { s.plate = None; } }
st.parked.remove(&done.plate);
Ok(done)
}
fn free(&self) -> HashMap<SpotSize, usize> {
let st = self.state.lock().unwrap();
let mut out = HashMap::new();
for s in st.levels.iter().flatten().filter(|s| s.plate.is_none()) { *out.entry(s.size).or_insert(0) += 1; }
out
}
}
fn main() {
use std::sync::{atomic::{AtomicU64, Ordering}, Arc};
let clock = Arc::new(AtomicU64::new(0)); // seconds since 09:00
let c = clock.clone();
let rates = HashMap::from([(VehicleType::Motorcycle, 20), (VehicleType::Car, 50), (VehicleType::Truck, 100)]);
let lot = Lot::new(vec![level(1, 1, 2, 1), level(2, 0, 1, 1)], Box::new(HourlyPricing { rates }),
Box::new(move || c.load(Ordering::SeqCst)));
let v = |p: &str, k| Vehicle { plate: p.to_string(), kind: k };
use VehicleType::*;
let arrivals = [v("BIKE-1", Motorcycle), v("CAR-A", Car), v("CAR-B", Car), v("TRUCK-1", Truck),
v("CAR-C", Car), v("CAR-D", Car), v("TRUCK-2", Truck)];
let mut tickets = HashMap::new();
for a in &arrivals {
match lot.park(a) {
Ok(t) => { println!("{:<8} {:<10} -> {} (ticket {})", a.plate, a.kind, t.spot_id, t.id); tickets.insert(a.plate.clone(), t.id); }
Err(e) => println!("{:<8} {:<10} -> {}", a.plate, a.kind, e),
}
}
clock.store(2 * 3600 + 30 * 60, Ordering::SeqCst);
let t = lot.leave(&tickets["CAR-A"]).unwrap();
let mins = (t.exit - t.entry) / 60;
println!("CAR-A left after {}h{}m0s, fee {} taka", mins / 60, mins % 60, t.fee);
if let Err(e) = lot.leave(&tickets["CAR-A"]) { println!("CAR-A again: {e}"); }
if let Err(e) = lot.park(&v("TRUCK-2", Truck)) { println!("TRUCK-2 retry -> {e}"); }
let f = lot.free();
let n = |s| f.get(&s).copied().unwrap_or(0);
println!("free: small={} medium={} large={}", n(SpotSize::Small), n(SpotSize::Medium), n(SpotSize::Large));
}import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.function.Supplier;
public class Parking {
enum VehicleType { MOTORCYCLE, CAR, TRUCK }
// Ordered by declaration: compareTo says LARGE > SMALL.
enum SpotSize { SMALL, MEDIUM, LARGE;
static SpotSize forVehicle(VehicleType t) {
return switch (t) { case MOTORCYCLE -> SMALL; case CAR -> MEDIUM; case TRUCK -> LARGE; };
}
}
record Vehicle(String plate, VehicleType type) {}
// A record is immutable: Leave returns a new ticket instead of changing the old one.
record Ticket(String id, String plate, VehicleType vehicle, String spotId,
Instant entry, Instant exit, int fee, boolean paid) {}
static final class Spot {
final String id; final SpotSize size; String plate; // null = free
Spot(String id, SpotSize size) { this.id = id; this.size = size; }
}
static class ParkingException extends RuntimeException {
private static final long serialVersionUID = 1L;
ParkingException(String msg) { super(msg); }
}
interface PricingStrategy { int fee(VehicleType v, Duration stay); }
static final class HourlyPricing implements PricingStrategy {
private final Map<VehicleType, Integer> rates;
HourlyPricing(Map<VehicleType, Integer> rates) { this.rates = Map.copyOf(rates); }
public int fee(VehicleType v, Duration stay) {
long hours = Math.max(1, (stay.toSeconds() + 3599) / 3600);
return (int) hours * rates.get(v);
}
}
static final class Level {
final List<Spot> spots = new ArrayList<>();
Level(int n, int small, int medium, int large) {
int i = 0;
for (int k = 0; k < small; k++) spots.add(new Spot("L" + n + "-S" + ++i, SpotSize.SMALL));
for (int k = 0; k < medium; k++) spots.add(new Spot("L" + n + "-S" + ++i, SpotSize.MEDIUM));
for (int k = 0; k < large; k++) spots.add(new Spot("L" + n + "-S" + ++i, SpotSize.LARGE));
}
Optional<Spot> findSpot(SpotSize need) {
return spots.stream().filter(s -> s.plate == null && s.size.compareTo(need) >= 0).findFirst();
}
}
// synchronized methods share one lock: the object itself.
static final class Lot {
private final List<Level> levels;
private final PricingStrategy pricing;
private final Supplier<Instant> now;
private final Map<String, Spot> spots = new HashMap<>();
private final Map<String, Ticket> tickets = new HashMap<>();
private final Map<String, String> parked = new HashMap<>(); // plate -> ticket id
private int next = 0;
Lot(List<Level> levels, PricingStrategy pricing, Supplier<Instant> now) {
this.levels = List.copyOf(levels); this.pricing = pricing; this.now = now;
for (Level lv : levels) for (Spot s : lv.spots) spots.put(s.id, s);
}
synchronized Ticket park(Vehicle v) {
if (v.plate() == null || v.plate().isEmpty()) throw new IllegalArgumentException("vehicle needs a plate");
if (parked.containsKey(v.plate())) throw new ParkingException("vehicle is already parked");
SpotSize need = SpotSize.forVehicle(v.type());
for (Level lv : levels) {
Optional<Spot> found = lv.findSpot(need);
if (found.isEmpty()) continue;
Spot s = found.get();
s.plate = v.plate();
Ticket t = new Ticket(String.format("T%04d", ++next), v.plate(), v.type(), s.id, now.get(), null, 0, false);
tickets.put(t.id(), t);
parked.put(v.plate(), t.id());
return t;
}
throw new ParkingException("no free spot for this vehicle");
}
synchronized Ticket leave(String ticketId) {
Ticket t = tickets.get(ticketId);
if (t == null) throw new ParkingException("unknown ticket");
if (t.paid()) throw new ParkingException("ticket already paid");
Instant exit = now.get();
Ticket done = new Ticket(t.id(), t.plate(), t.vehicle(), t.spotId(), t.entry(), exit,
pricing.fee(t.vehicle(), Duration.between(t.entry(), exit)), true);
tickets.put(done.id(), done);
spots.get(done.spotId()).plate = null;
parked.remove(done.plate());
return done;
}
synchronized Map<SpotSize, Integer> free() {
Map<SpotSize, Integer> out = new EnumMap<>(SpotSize.class);
for (SpotSize s : SpotSize.values()) out.put(s, 0);
for (Spot s : spots.values()) if (s.plate == null) out.merge(s.size, 1, Integer::sum);
return out;
}
}
public static void main(String[] args) {
Instant[] clock = { Instant.parse("2026-11-07T09:00:00Z") };
Lot lot = new Lot(List.of(new Level(1, 1, 2, 1), new Level(2, 0, 1, 1)),
new HourlyPricing(Map.of(VehicleType.MOTORCYCLE, 20, VehicleType.CAR, 50, VehicleType.TRUCK, 100)),
() -> clock[0]);
var M = VehicleType.MOTORCYCLE; var C = VehicleType.CAR; var T = VehicleType.TRUCK;
List<Vehicle> arrivals = List.of(new Vehicle("BIKE-1", M), new Vehicle("CAR-A", C), new Vehicle("CAR-B", C),
new Vehicle("TRUCK-1", T), new Vehicle("CAR-C", C), new Vehicle("CAR-D", C), new Vehicle("TRUCK-2", T));
Map<String, String> tickets = new HashMap<>();
for (Vehicle v : arrivals) {
String name = v.type().name().toLowerCase();
try {
Ticket t = lot.park(v);
tickets.put(v.plate(), t.id());
System.out.printf("%-8s %-10s -> %s (ticket %s)%n", v.plate(), name, t.spotId(), t.id());
} catch (ParkingException e) {
System.out.printf("%-8s %-10s -> %s%n", v.plate(), name, e.getMessage());
}
}
clock[0] = clock[0].plus(Duration.ofMinutes(150));
Ticket t = lot.leave(tickets.get("CAR-A"));
long mins = Duration.between(t.entry(), t.exit()).toMinutes();
System.out.printf("CAR-A left after %dh%dm0s, fee %d taka%n", mins / 60, mins % 60, t.fee());
try { lot.leave(tickets.get("CAR-A")); } catch (ParkingException e) { System.out.println("CAR-A again: " + e.getMessage()); }
try { lot.park(new Vehicle("TRUCK-2", T)); } catch (ParkingException e) { System.out.println("TRUCK-2 retry -> " + e.getMessage()); }
var f = lot.free();
System.out.printf("free: small=%d medium=%d large=%d%n", f.get(SpotSize.SMALL), f.get(SpotSize.MEDIUM), f.get(SpotSize.LARGE));
}
}#include <chrono>
#include <cstdio>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
using Clock = std::function<std::chrono::minutes()>; // minutes since opening
enum class VehicleType { Motorcycle, Car, Truck };
enum class SpotSize { Small, Medium, Large }; // enum class compares by value: Large > Small
const char* name(VehicleType t) {
switch (t) {
case VehicleType::Motorcycle: return "motorcycle";
case VehicleType::Car: return "car";
default: return "truck";
}
}
SpotSize sizeFor(VehicleType t) {
switch (t) {
case VehicleType::Motorcycle: return SpotSize::Small;
case VehicleType::Car: return SpotSize::Medium;
default: return SpotSize::Large;
}
}
struct Vehicle { std::string plate; VehicleType type; };
struct Spot { std::string id; SpotSize size; std::string plate; }; // "" = free
struct Ticket {
std::string id, plate, spotId;
VehicleType vehicle;
std::chrono::minutes entry{}, exit{};
int fee = 0;
bool paid = false;
};
struct ParkingError : std::runtime_error { using std::runtime_error::runtime_error; };
struct PricingStrategy {
virtual ~PricingStrategy() = default;
virtual int fee(VehicleType v, std::chrono::minutes stay) const = 0;
};
struct HourlyPricing : PricingStrategy {
std::map<VehicleType, int> rates;
explicit HourlyPricing(std::map<VehicleType, int> r) : rates(std::move(r)) {}
int fee(VehicleType v, std::chrono::minutes stay) const override {
long hours = std::max<long>(1, (stay.count() + 59) / 60);
return static_cast<int>(hours) * rates.at(v);
}
};
struct Level {
std::vector<Spot> spots;
Level(int n, int small, int medium, int large) {
int i = 0;
auto add = [&](int count, SpotSize s) {
for (int k = 0; k < count; ++k) spots.push_back({"L" + std::to_string(n) + "-S" + std::to_string(++i), s, ""});
};
add(small, SpotSize::Small); add(medium, SpotSize::Medium); add(large, SpotSize::Large);
}
Spot* findSpot(SpotSize need) {
for (auto& s : spots) if (s.plate.empty() && s.size >= need) return &s;
return nullptr;
}
};
class Lot {
public:
Lot(std::vector<Level> levels, std::unique_ptr<PricingStrategy> pricing, Clock now)
: levels_(std::move(levels)), pricing_(std::move(pricing)), now_(std::move(now)) {}
// Returns a copy: the caller can't reach into the lot's own ticket.
Ticket park(const Vehicle& v) {
if (v.plate.empty()) throw std::invalid_argument("vehicle needs a plate");
std::lock_guard lock(mu_); // RAII: unlocked on every return and throw
if (parked_.count(v.plate)) throw ParkingError("vehicle is already parked");
for (auto& lv : levels_) {
Spot* s = lv.findSpot(sizeFor(v.type));
if (!s) continue;
s->plate = v.plate;
char id[16];
std::snprintf(id, sizeof id, "T%04d", ++next_);
Ticket t{id, v.plate, s->id, v.type, now_()};
tickets_[t.id] = t;
parked_[v.plate] = t.id;
return t;
}
throw ParkingError("no free spot for this vehicle");
}
Ticket leave(const std::string& ticketId) {
std::lock_guard lock(mu_);
auto it = tickets_.find(ticketId);
if (it == tickets_.end()) throw ParkingError("unknown ticket");
Ticket& t = it->second;
if (t.paid) throw ParkingError("ticket already paid");
t.exit = now_();
t.fee = pricing_->fee(t.vehicle, t.exit - t.entry);
t.paid = true;
for (auto& lv : levels_) for (auto& s : lv.spots) if (s.id == t.spotId) s.plate.clear();
parked_.erase(t.plate);
return t;
}
std::map<SpotSize, int> free() {
std::lock_guard lock(mu_);
std::map<SpotSize, int> out{{SpotSize::Small, 0}, {SpotSize::Medium, 0}, {SpotSize::Large, 0}};
for (auto& lv : levels_) for (auto& s : lv.spots) if (s.plate.empty()) ++out[s.size];
return out;
}
private:
std::mutex mu_;
std::vector<Level> levels_;
std::unique_ptr<PricingStrategy> pricing_;
Clock now_;
std::map<std::string, Ticket> tickets_;
std::map<std::string, std::string> parked_; // plate -> ticket id
int next_ = 0;
};
int main() {
using namespace std::chrono_literals;
std::chrono::minutes clock = 0min;
Lot lot({Level(1, 1, 2, 1), Level(2, 0, 1, 1)},
std::make_unique<HourlyPricing>(std::map<VehicleType, int>{
{VehicleType::Motorcycle, 20}, {VehicleType::Car, 50}, {VehicleType::Truck, 100}}),
[&] { return clock; });
auto M = VehicleType::Motorcycle, C = VehicleType::Car, T = VehicleType::Truck;
std::vector<Vehicle> arrivals{{"BIKE-1", M}, {"CAR-A", C}, {"CAR-B", C}, {"TRUCK-1", T},
{"CAR-C", C}, {"CAR-D", C}, {"TRUCK-2", T}};
std::map<std::string, std::string> tickets;
for (auto& v : arrivals) {
try {
Ticket t = lot.park(v);
tickets[v.plate] = t.id;
std::printf("%-8s %-10s -> %s (ticket %s)\n", v.plate.c_str(), name(v.type), t.spotId.c_str(), t.id.c_str());
} catch (const ParkingError& e) {
std::printf("%-8s %-10s -> %s\n", v.plate.c_str(), name(v.type), e.what());
}
}
clock += 150min;
Ticket t = lot.leave(tickets["CAR-A"]);
long mins = (t.exit - t.entry).count();
std::printf("CAR-A left after %ldh%ldm0s, fee %d taka\n", mins / 60, mins % 60, t.fee);
try { lot.leave(tickets["CAR-A"]); } catch (const ParkingError& e) { std::printf("CAR-A again: %s\n", e.what()); }
try { lot.park({"TRUCK-2", T}); } catch (const ParkingError& e) { std::printf("TRUCK-2 retry -> %s\n", e.what()); }
auto f = lot.free();
std::printf("free: small=%d medium=%d large=%d\n", f[SpotSize::Small], f[SpotSize::Medium], f[SpotSize::Large]);
}কীভাবে চালাবে: python3 parking.py, node parking.ts (Node 23+ সরাসরি TypeScript চালায়), rustc --edition 2021 parking.rs && ./parking, javac Parking.java && java Parking (Java 21), g++ -std=c++20 parking.cpp -o parking && ./parking।
এক language থেকে আরেকটায় কী বদলায়, আর কেন:
| "Pricing বদলানো যায়" কীভাবে লেখা | Ticket কীভাবে নিরাপদ | Lock কীভাবে কাজ করে | |
|---|---|---|---|
| Go | interface, আলাদা করে ঘোষণা না করেই মেনে চলে | struct এর value ফেরত দেয়, তাই caller পায় copy | sync.Mutex + defer Unlock() |
| Python | Protocol: fee method থাকলেই চলবে | @dataclass(frozen=True); paid copy বানায় replace() | with সহ threading.Lock |
| TypeScript | interface | readonly field; Leave নতুন object ফেরত দেয় | lock নেই: JS এই code এক thread এ চালায়, আর park এ কোনো await নেই, তাই মাঝপথে কেউ ঢুকতে পারে না |
| Rust | trait object Box<dyn PricingStrategy>, সাথে Send + Sync | clone() ফেরত দেয়; lot এর ভেতরের কোনো reference কেউ পায় না | বদলায় এমন সব state থাকে Mutex এর ভেতরে, তাই lock ছাড়া spot ছুঁতে গেলে compiler ই আটকে দেয় |
| Java | interface | record immutable; Leave নতুন একটা বানায় | synchronized method গুলো object এর একটাই lock ভাগ করে |
| C++ | pure virtual fee সহ abstract class | value হিসেবে ফেরত দেয় | std::lock_guard (RAII): প্রতিটা return আর throw তে নিজে থেকেই unlock |
Rust এর লাইনটা আরেকবার দেখো। Go, Java আর C++ এ lock আর সে যে data পাহারা দেয় সেগুলো দুইটা আলাদা field, শুধু নিজের সাবধানতা দিয়ে এদের একসাথে রাখতে হয়। Rust এ data টাই mutex এর ভেতরে, তাই lock ভুলে গেলে সেটা compile error, production এ গিয়ে ধরা পড়া bug না।
Testing
// lot_test.go
package parking
import (
"errors"
"fmt"
"sync"
"testing"
"time"
)
var rates = HourlyPricing{Rates: map[VehicleType]int{Motorcycle: 20, Car: 50, Truck: 100}}
// fakeClock is a clock the test moves by hand.
type fakeClock struct{ t time.Time }
func (c *fakeClock) now() time.Time { return c.t }
func (c *fakeClock) add(d time.Duration) { c.t = c.t.Add(d) }
func newClock() *fakeClock { return &fakeClock{time.Date(2026, 1, 1, 9, 0, 0, 0, time.UTC)} }
func newLot(c *fakeClock, lv ...*Level) *Lot { return NewLot(lv, rates, WithClock(c.now)) }
func TestBestFitSpot(t *testing.T) {
lot := newLot(newClock(), NewLevel(1, 1, 1, 1))
cases := []struct {
v Vehicle
spot string
}{
{Vehicle{"B1", Motorcycle}, "L1-S1"}, // small, its own size
{Vehicle{"B2", Motorcycle}, "L1-S2"}, // small is taken: medium is next best
{Vehicle{"C1", Car}, "L1-S3"}, // only large is left, a car still fits
}
for _, c := range cases {
tk, err := lot.Park(c.v)
if err != nil || tk.SpotID != c.spot {
t.Fatalf("%s: got %q, %v; want %s", c.v.Plate, tk.SpotID, err, c.spot)
}
}
}
func TestLowerLevelFirst(t *testing.T) {
lot := newLot(newClock(), NewLevel(1, 0, 1, 0), NewLevel(2, 0, 1, 0))
a, _ := lot.Park(Vehicle{"A", Car})
b, _ := lot.Park(Vehicle{"B", Car})
if a.SpotID != "L1-S1" || b.SpotID != "L2-S1" {
t.Fatalf("got %s, %s", a.SpotID, b.SpotID)
}
}
func TestTruckNeedsLarge(t *testing.T) {
lot := newLot(newClock(), NewLevel(1, 3, 3, 0))
if _, err := lot.Park(Vehicle{"T", Truck}); !errors.Is(err, ErrLotFull) {
t.Fatalf("want ErrLotFull, got %v", err)
}
}
func TestInvalidInput(t *testing.T) {
lot := newLot(newClock(), NewLevel(1, 1, 1, 1))
if _, err := lot.Park(Vehicle{"", Car}); !errors.Is(err, ErrInvalidVehicle) {
t.Fatalf("empty plate: %v", err)
}
if _, err := lot.Leave("T9999"); !errors.Is(err, ErrUnknownTicket) {
t.Fatalf("unknown ticket: %v", err)
}
}
func TestSamePlateTwice(t *testing.T) {
lot := newLot(newClock(), NewLevel(1, 0, 2, 0))
lot.Park(Vehicle{"A", Car})
if _, err := lot.Park(Vehicle{"A", Car}); !errors.Is(err, ErrAlreadyParked) {
t.Fatalf("want ErrAlreadyParked, got %v", err)
}
}
func TestFeeRoundsUpHours(t *testing.T) {
cases := []struct {
stay time.Duration
fee int
}{
{0, 50}, // minimum one hour
{time.Hour, 50}, // exactly one hour
{time.Hour + time.Minute, 100}, // a started hour counts
{2*time.Hour + 30*time.Minute, 150},
}
for _, c := range cases {
clk := newClock()
lot := newLot(clk, NewLevel(1, 0, 1, 0))
tk, _ := lot.Park(Vehicle{"A", Car})
clk.add(c.stay)
out, err := lot.Leave(tk.ID)
if err != nil || out.Fee != c.fee {
t.Errorf("stay %v: fee %d, %v; want %d", c.stay, out.Fee, err, c.fee)
}
}
}
func TestLeaveFreesSpotOnce(t *testing.T) {
lot := newLot(newClock(), NewLevel(1, 0, 1, 0))
tk, _ := lot.Park(Vehicle{"A", Car})
if _, err := lot.Leave(tk.ID); err != nil {
t.Fatal(err)
}
if _, err := lot.Leave(tk.ID); !errors.Is(err, ErrTicketPaid) {
t.Fatalf("second leave: %v", err)
}
if again, err := lot.Park(Vehicle{"B", Car}); err != nil || again.SpotID != "L1-S1" {
t.Fatalf("spot not reused: %q, %v", again.SpotID, err)
}
}
func TestReturnedTicketIsACopy(t *testing.T) {
lot := newLot(newClock(), NewLevel(1, 0, 1, 0))
tk, _ := lot.Park(Vehicle{"A", Car})
tk.Status = Paid // the caller scribbles on its copy
if _, err := lot.Leave(tk.ID); err != nil {
t.Fatalf("lot's ticket changed from outside: %v", err)
}
}
func TestSwapPricing(t *testing.T) {
clk := newClock()
lot := NewLot([]*Level{NewLevel(1, 0, 1, 0)}, FlatPricing{Amount: 200}, WithClock(clk.now))
tk, _ := lot.Park(Vehicle{"A", Car})
clk.add(5 * time.Hour)
if out, _ := lot.Leave(tk.ID); out.Fee != 200 {
t.Fatalf("flat fee: %d", out.Fee)
}
}
// 100 cars race for 50 spots through many gates at once: exactly 50 get in, and no spot
// is handed out twice. Run with -race.
func TestConcurrentGates(t *testing.T) {
lot := newLot(newClock(), NewLevel(1, 0, 25, 0), NewLevel(2, 0, 25, 0))
var wg sync.WaitGroup
var mu sync.Mutex
taken := map[string]string{}
full := 0
for i := range 100 {
wg.Add(1)
go func() {
defer wg.Done()
tk, err := lot.Park(Vehicle{fmt.Sprintf("CAR-%d", i), Car})
mu.Lock()
defer mu.Unlock()
if errors.Is(err, ErrLotFull) {
full++
return
}
if other, dup := taken[tk.SpotID]; dup {
t.Errorf("%s given to %s and %s", tk.SpotID, other, tk.Plate)
}
taken[tk.SpotID] = tk.Plate
}()
}
wg.Wait()
if len(taken) != 50 || full != 50 {
t.Fatalf("parked %d, turned away %d", len(taken), full)
}
}
$ go test -race -v ./...
=== RUN TestBestFitSpot
--- PASS: TestBestFitSpot (0.00s)
=== RUN TestLowerLevelFirst
--- PASS: TestLowerLevelFirst (0.00s)
=== RUN TestTruckNeedsLarge
--- PASS: TestTruckNeedsLarge (0.00s)
=== RUN TestInvalidInput
--- PASS: TestInvalidInput (0.00s)
=== RUN TestSamePlateTwice
--- PASS: TestSamePlateTwice (0.00s)
=== RUN TestFeeRoundsUpHours
--- PASS: TestFeeRoundsUpHours (0.00s)
=== RUN TestLeaveFreesSpotOnce
--- PASS: TestLeaveFreesSpotOnce (0.00s)
=== RUN TestReturnedTicketIsACopy
--- PASS: TestReturnedTicketIsACopy (0.00s)
=== RUN TestSwapPricing
--- PASS: TestSwapPricing (0.00s)
=== RUN TestConcurrentGates
--- PASS: TestConcurrentGates (0.00s)
PASS
ok parking 1.012s
? parking/cmd/demo [no test files]
কোন test কী প্রমাণ করে:
| Test | যা check করে |
|---|---|
TestBestFitSpot | আগে আঁটে এমন সবচেয়ে ছোট spot, সেটা দখল থাকলে তার পরের size |
TestLowerLevelFirst | Level 2 এর আগে Level 1 ভরে |
TestTruckNeedsLarge | অনেক small spot খালি থাকলেও vehicle কখনো ছোট spot পায় না |
TestInvalidInput | খালি plate আর অচেনা ticket ঠিক error দিয়ে ফেরত যায় |
TestSamePlateTwice | একটা plate দুইটা spot ধরে রাখতে পারে না |
TestFeeRoundsUpHours | সীমানা গুলো: ০ মিনিট, ঠিক ১ ঘণ্টা, ১ ঘণ্টা ১ মিনিট, আড়াই ঘণ্টা |
TestLeaveFreesSpotOnce | Leave spot খালি করে, দ্বিতীয় Leave ফেরত যায়, আর spot টা আবার ব্যবহার হয় |
TestReturnedTicketIsACopy | ফেরত পাওয়া ticket বদলালে lot এর ticket বদলায় না |
TestSwapPricing | Lot না বদলেই অন্য strategy চলে |
TestConcurrentGates | ১০০টা goroutine, ৫০টা spot: ঠিক ৫০টা park হয়, কোনো spot দুইজন পায় না |
Concurrency test টা কি আসলেই কিছু ধরে? আমরা দেখে নিয়েছি: Park থেকে lock এর দুইটা লাইন মুছে আবার চালাও।
$ go test -race -run Concurrent .
WARNING: DATA RACE
...
FAIL parking 0.015s
প্রতিবার race detector data race দেখালো (আমাদের run এ ৫ থেকে ৮টা), আর test fail করলো। Lock ফেরত দিলে pass করে। যে test fix থাকলেও pass করে আর না থাকলেও pass করে, সে কিছুই প্রমাণ করে না। তাই একবার এভাবে check করে নেওয়া উচিত।
Design review
ধরো একজন teammate review এর জন্য এটা পাঠিয়েছে:
যা ভালো
- প্রতিটা type এর একটা কাজ, আর সিদ্ধান্ত হয় শুধু
Lotএ। - Pricing এর নিয়ম বা clock বদলাতে
Lotএ একটা লাইনও বদলাতে হয় না। - ভুল করে lock এড়ানোর উপায় নেই: spot এর state private, আর প্রতিটা public method lock নেয়।
- Test গুলো millisecond এ চলে, আর টাকার সব কোণাকাঞ্চি আর race দুটোই ধরে।
কী ভাঙতে পারে, আর requirement বদলালে কী হয়
| "যদি…" | কী ভাঙে | কী বদলাতে হবে |
|---|---|---|
গাড়ি গুলো large spot দখল করতেই থাকে (demo তে CAR-D আর TRUCK-2 দেখো) | Truck এর spot এ গাড়ি বসে থাকে, আর truck ফিরে যায় | Spot বাছাইকে দ্বিতীয় একটা strategy বানাও: "truck এর জন্য শুধু ঠিক size", বা শেষ N টা large spot truck এর জন্য রেখে দাও। এই মুহূর্তেই একটা SpotFinder interface এর দরকার হয়, তার আগে না। |
| ১০,০০০ spot আর ২০টা gate | একটা lock এর জন্য প্রতিটা gate কে পুরো scan শেষ হওয়া পর্যন্ত বসে থাকতে হয় | প্রতিটা level এ প্রতিটা size এর জন্য একটা free list রাখো (খালি spot ID এর queue), আর level ধরে lock দাও। Park তখন O(spots) থেকে O(levels) হয়ে যায়। |
| Electric car charging spot | SpotSize একটা সরল ক্রম; "EV" বড়ও না, ছোটও না | Size এর পাশে spot এর feature যোগ করো (একটা set: ev, handicapped), আর দুটো মিলিয়ে দেখো। |
| Ticket হারিয়ে গেছে | Leave এ ticket ID লাগে | Plate দিয়ে active ticket খোঁজো (parked[plate] আমরা আগেই রাখি), আর হারানো ticket এর fee একটা pricing rule হিসেবে নাও। |
| Reservation | খালি একটা spot হয়তো কাউকে কথা দেওয়া আছে | Spot এর তৃতীয় একটা state লাগবে, reserved, মেয়াদ সহ। তখন এটা ছোট একটা state machine। |
| কারেন্ট চলে গেল | সব memory তে, সব ticket উধাও | Ticket আর spot এর state save করো। এটাই HLD version: একটা database, আর lot হয়ে যায় একটা service। |
যা ইচ্ছা করে সহজ রাখা
- Ticket ID একটা counter, এক process এ এটা ঠিক আছে। কয়েকটা server জুড়ে হলে UUID বা একটা কেন্দ্রীয় ID service লাগবে।
- খালি spot গুনতে প্রতিবার সব spot দেখা হয়। কয়েক সেকেন্ড পর পর refresh হওয়া gate board এর জন্য এটুকুই যথেষ্ট।
Production এ যাওয়ার আগে যা বদলাবো: প্রথম লাইনের spot বাছাইয়ের নিয়ম (আসল car park এ এটা নিয়ে সত্যিই অভিযোগ আসে), আর প্রতিটা Park আর Leave ticket ID সহ log করা, যাতে fee নিয়ে ঝামেলা হলে খুঁজে বের করা যায়।
Interview প্রশ্ন
Practice প্রশ্ন
Beginner
- খালি spot খোঁজার দায়িত্ব
Spotএর না কেন? Parkpointer না দিয়েTicketএর value কেন ফেরত দেয়?- "আঁটে এমন সবচেয়ে ছোট spot" নিয়মটা code কীভাবে নিশ্চিত করে?
Intermediate
- রাত ১০টা থেকে সকাল ৬টা পর্যন্ত কম দামের night rate কীভাবে যোগ করবে?
- শেষ spot এর জন্য দুইটা gate একসাথে
Parkডাকলো। ধাপে ধাপে কী হয় বলো। - আড়াই ঘণ্টা অপেক্ষা না করে কীভাবে test করবে যে আড়াই ঘণ্টায় ১৫০ টাকা হয়?
Advanced
- Lot বড় হয়ে ১০,০০০ spot আর ২০টা gate হলো। Bottleneck কোথায়, আর কীভাবে ঠিক করবে?
- গাড়ি large spot নিয়ে নিচ্ছে আর truck ফিরে যাচ্ছে। Design টা এমনভাবে বদলাও যাতে নিয়মটা config করা যায়।
LotSingleton হলে এই code কেন খারাপ হতো?
Interviewer রা যে follow-up পছন্দ করে
- "একটা floor শুধু motorcycle এর জন্য করো।" · "শুধু size না, প্রতিটা level এ কয়টা খালি সেটা দেখাও।" · "Level অনুযায়ী fee বদলাও (ছাদে দাম কম)।"
Design এর সাধারণ ভুল
- প্রতিটা spot size এর জন্য subclass (
SmallSpot,LargeSpot), যেখানে পার্থক্য শুধু data তে। - Pricing
Leaveএর ভেতরেই লেখা,ifএর chain দিয়ে। তখন প্রতিটা নিয়ম বদলাতে parking এর code edit করতে হয়। - সরাসরি system clock ডাকা, এতে fee এর test হয় ধীর হয়, নয়তো কখনো pass করে কখনো করে না।
- Concurrency পুরো ভুলে যাওয়া, অথবা শুধু "খোঁজা" ধাপে lock দেওয়া, "দখল দেখানো" ধাপে না। তাতে race টা থেকেই যায়।
- ভেতরের pointer ফেরত দেওয়া, তাতে caller lock ছাড়াই ticket বা spot বদলে ফেলতে পারে।
- শুধু call পাঠিয়ে দেওয়া একটা
Gateclass, যে একটা layer যোগ করে কিন্তু কোনো সিদ্ধান্ত নেয় না।
উত্তর
১. খালি spot খোঁজার দায়িত্ব Spot এর না কেন?
খুঁজতে গেলে সব spot দেখতে হয় আর তুলনা করতে হয়। একটা spot শুধু নিজেকে চেনে। Spot খুঁজলে তাকে তার ভাইবোনদের reference রাখতে হতো, আর "best fit" এর নিয়ম অনেক object এ ছড়িয়ে যেতো। খোঁজার কাজ তার, যে পুরো floor (Level) আর পুরো lot (Lot) দেখতে পায়।
২. Park pointer না দিয়ে value কেন দেয়?
Pointer দিলে caller lot এর নিজের ticket টাই বদলে ফেলতে পারতো, যেমন Status = Paid, lock ছাড়া আর spot খালি না করেই। Value মানে copy: caller পড়তে পারে, ইচ্ছামতো লিখতেও পারে, কিন্তু lot এর আসলটা ঠিক থাকে। TestReturnedTicketIsACopy ঠিক এটাই check করে।
৩. সবচেয়ে ছোট spot এর নিয়ম কীভাবে নিশ্চিত হয়?
NewLevel spot গুলো ছোট থেকে বড় ক্রমে যোগ করে। findSpot প্রথম যে খালি spot এর Size >= need, সেটা ফেরত দেয়। ক্রমের কারণে প্রথম মিলটাই আঁটে এমন সবচেয়ে ছোট। নিশ্চয়তাটা থাকে constructor এর ক্রমে, তাই ওখানে একটা comment থাকা জরুরি।
৪. Night rate কীভাবে যোগ করবে?
Fee method ওয়ালা নতুন একটা type লেখো, যেটা থাকার সময়কে রাত আর দিনের ঘণ্টায় ভাগ করে আলাদা দাম ধরে। তারপর সেটা NewLot এ পাঠাও। Lot বদলায় না। তবে একটা ঝামেলা আছে: Fee শুধু কতক্ষণ ছিল সেটা পায়, অথচ night rate এর লাগে আসল সময়টা। তাই interface টা বদলে Fee(vehicle, entry, exit) করতে হবে। Interview তে এটা মুখে বললে বাড়তি নম্বর: বোঝা যায় তুমি খেয়াল করেছো interface এ একটা তথ্য কম আছে।
৫. দুইটা gate, একটা spot: কী হয়?
দুজনেই Park ডাকে। একজন আগে mu পায়, spot খুঁজে পায়, দখল দেখায়, ticket বানায় আর unlock করে। অন্যজন এতক্ষণ Lock() এ আটকে ছিল। এবার সে খোঁজে, কিছু পায় না, আর ErrLotFull ফেরত দেয়। মূল কথা হলো "খোঁজা" আর "দখল দেখানো" দুটোই হয় একই critical section এর ভেতরে। আলাদা আলাদা lock দিলে দুজনেই একই খালি spot পেয়ে যেতে পারতো।
৬. আড়াই ঘণ্টার থাকা এক নিমিষে কীভাবে test করবে?
Lot কখনো সরাসরি time.Now() ডাকে না। সে ডাকে l.now(), যেটা default ভাবে time.Now, কিন্তু WithClock দিয়ে বদলানো যায়। Test একটা নকল clock দেয়, park করে, নকল clock ১৫০ মিনিট এগিয়ে দেয়, তারপর Leave ডাকে। কোনো sleep নেই, আর প্রতিবার একই ফল।
৭. ১০,০০০ spot আর ২০টা gate: bottleneck কোথায়?
দুই জায়গায়। প্রতিটা গাড়ির জন্য scan হয় O(spots), আর পুরোটা হয় একটা lock এর ভেতরে, তাই gate গুলো লাইনে দাঁড়ায়। Scan ঠিক করো প্রতিটা level এ প্রতিটা size এর free list দিয়ে: খালি spot ID এর একটা queue, তাতে spot খোঁজা মানে queue থেকে একটা তুলে নেওয়া। ভিড় কমাও পুরো lot এর বদলে level ধরে lock দিয়ে। তখন একটা gate শুধু একই level এ কাজ করা gate গুলোকে আটকায়। খালি spot এর হিসাব হয়ে যায় কয়েকটা counter, যেগুলো Park আর Leave এর সময় update হয়।
৮. Spot এর নিয়ম config করার ব্যবস্থা
"কোন vehicle কোন spot এ" প্রশ্নটা দ্বিতীয় একটা interface এর পেছনে নাও, যেমন Find(levels, vehicle) *Spot সহ SpotFinder। এখনকার behaviour হয়ে যায় BestFitFinder। নতুন type হিসেবে যোগ করো ExactSizeForTrucks বা ReserveLargeSpots{Keep: 2}। এটা pricing এর মতোই Strategy এর চাল, শুধু পার্থক্য হলো এটা নেওয়া হচ্ছে যখন সত্যিকারের দরকার এসেছে, আগেভাগে না।
৯. Singleton lot কেন খারাপ?
সব test একটা global lot ভাগ করতো, তাই test গুলো আর একসাথে চালানো যেতো না, পরিষ্কার অবস্থা থেকে শুরুও করা যেতো না। দ্বিতীয় building আনতে আবার লিখতে হতো। আর dependency টা লুকিয়ে যায়: function গুলো *Lot হাতে না পেয়ে global টা টেনে নেয়, তাই signature দেখে বোঝা যায় না কে parking এর state ছোঁয়। main এ একটা Lot বানিয়ে পাঠিয়ে দিলে "একটাই" ব্যাপারটা থাকে, এই ঝামেলা গুলো ছাড়াই।
Interviewer রা যা দেখে: একটা করে কাজওয়ালা পরিষ্কার entity, interface এর পেছনে pricing এর নিয়ম, concurrency এর আসল উত্তর (lock কোথায় আর কেন), আর পুরো design না ভেঙে "যদি…" প্রশ্ন সামলানোর ক্ষমতা।
নিজে design করে solution টা বুঝেছো? Mark করো, তাহলে progress ট্র্যাক হবে।