Practice · Low Level Design · State Machines
Design a Vending Machine
Idle, টাকা ঢুকেছে, জিনিস দিচ্ছে, stock শেষ: State pattern এর আসল জায়গা, সাথে ঠিকঠাক ভাংতি ফেরত দেওয়া আর কিছু গোলমাল হলে টাকা ফেরত।
Learn mode: পুরো solution নিচে দেখানো আছে।
Problem এর পরিচয়
| Difficulty | Beginner |
| সময় | Design আর code মিলিয়ে ১–২ ঘণ্টা; interview তে মোটামুটি ৪৫ মিনিট |
| আগে জানা দরকার | Enum আর state machine, interface, lock সম্পর্কে একটু ধারণা |
| যা শিখবে | State pattern, ভাংতি ফেরত দেওয়া, fail করা বিক্রি যেন কিছুই না বদলায় |
| বাস্তবে কোথায় | অফিস আর হাসপাতালের snacks আর drinks এর machine, ticket machine, টাকা নেয় এমন যেকোনো kiosk |
Vending machine হলো State pattern এর চেনা প্রশ্ন। একই button ("select A1") টাকা ঢোকানো আছে কিনা তার উপর নির্ভর করে আলাদা কাজ করে, আর interviewer দেখতে চান তুমি এর উত্তর if status == … এর স্তূপ দিয়ে দাও কিনা। ভাংতি দেওয়া, আর ভাংতি দিতে না পারলে পরিষ্কারভাবে টাকা ফেরত দেওয়া, এখানে বাস্তবের ঝামেলাটা যোগ করে।
Problem statement
ঢাকার কোনো হাসপাতালের করিডোরে একটা snacks এর machine এর কথা ভাবো। এতে A1 (chips, ৩০ টাকা), B2 (juice, ৬৫ টাকা), C3 (পানি, ২০ টাকা) এর মতো slot আছে। এটা ৫, ১০, ২০, ৫০ আর ১০০ টাকার coin আর নোট নেয়, আর ভেতরের coin থেকে ভাংতি দেয়।
Customer টাকা ঢোকায়, একটা slot এর code চাপে, তারপর হয় জিনিস আর ভাংতি পায়, নয়তো পরিষ্কার একটা message: টাকা কম, শেষ হয়ে গেছে, বা "ভাংতি দেওয়া যাচ্ছে না"। কেনার আগে যেকোনো সময় cancel চাপতে পারে, আর নিজের coin গুলোই ফেরত পায়।
আমরা কী বানাচ্ছি: machine এর ভেতরের controller: coin নেওয়া, জিনিস বাছা, ভাংতি আর টাকা ফেরত দেওয়া।
কেন দরকার: যে machine টাকা খেয়ে ফেলে বা ভুল ভাংতি দেয়, সে খুব তাড়াতাড়ি customer আর টাকা দুটোই হারায়, আর সেখানে ঠিক করার জন্য কেউ দাঁড়িয়েও থাকে না।
এক লাইনে: টাকা নাও, customer যথেষ্ট দিলে আর ভাংতি দেওয়া গেলে জিনিস বিক্রি করো, নইলে পরিষ্কারভাবে টাকা ফেরত দাও।
Requirement
Functional requirement
- প্রতিটা slot এর একটা code, একটা product এর নাম, দাম আর stock আছে।
- ৫, ১০, ২০, ৫০ আর ১০০ টাকা নেবে; বাকি সব ফিরিয়ে দেবে।
- Insert customer এর balance এ যোগ করে।
- Select জিনিসটা বিক্রি করে, যদি slot টা থাকে, stock থাকে, আর balance দাম ঢেকে দেয়। ভাংতি দেওয়া হয় machine এর coin থেকে, বড়টা আগে।
- Machine ঠিক ভাংতি দিতে না পারলে বিক্রি বাতিল, customer তার coin ফেরত পায়। আর কিছুই বদলায় না।
- Cancel customer যে coin গুলো ঢুকিয়েছিল ঠিক সেগুলোই ফেরত দেয়।
- পরিষ্কার message: আগে টাকা দাও, টাকা কম (আর কত লাগবে), শেষ হয়ে গেছে, অচেনা slot।
Non-functional requirement
| Requirement | এখানে এর মানে |
|---|---|
| কোনো টাকা হারাবে না | প্রতিটা রাস্তা শেষ হয় হয় বিক্রি আর ঠিক ভাংতি দিয়ে, নয়তো পুরো টাকা ফেরত দিয়ে |
| Extensibility | নতুন state (maintenance mode, card payment) আনতে প্রতিটা method নতুন করে লিখতে হবে না |
| Concurrency তেও ঠিক থাকা | Touch screen আর coin slot আলাদা thread এ চলে; balance কখনো গোলমাল হবে না |
| Testability | Hardware ছাড়াই প্রতিটা rule test করা যায় |
যা ধরে নিচ্ছি আর যা এখানে নেই
- আসল machine এর সামনে একবারে একজন customer।
- Card বা bKash payment নেই, restock এর screen নেই, audit report নেই।
যা জমা দিতে হবে
- State গুলো, আর প্রতিটা state এ প্রতিটা event কী করে।
- একটা class diagram।
- Insert, select আর cancel এর code, ভাংতি দেওয়া সহ।
- "টাকা কম", "শেষ হয়ে গেছে", "ভাংতি দেওয়া যায় না" আর cancel এর test।
Hint 1: 'select' আসলে কী করে?
ছোট একটা table বানাও: সারি গুলো state (টাকা নেই, টাকা আছে), কলাম গুলো event (insert, select, cancel)। বেশিরভাগ ঘর আলাদা হলে বুঝবে State pattern এর দিকে যাওয়ার ইঙ্গিত।
Hint 2: ভাংতি আসে কোথা থেকে?
Machine এর ভেতরের coin থেকে, সাথে customer এইমাত্র যেগুলো ঢুকিয়েছে সেগুলোও। আগে সবচেয়ে বড় coin দিয়ে চেষ্টা করো।
Hint 3: মাঝপথে ভাংতি fail করলে?
Coin এর হিসাবের একটা copy নিয়ে কাজ করো। পুরো টাকা মেটানো গেলে তবেই আসল হিসাবের জায়গায় copy টা বসাও।
Hint 4: cancel
Customer কে তার নিজের coin ফেরত দিতে হলে শুধু মোট টাকা না, সে কী কী ঢুকিয়েছে তার একটা list রাখো।
মূল entity
| Entity | এক বাক্যে কাজ | কী রাখে | কী করে |
|---|---|---|---|
Product | একটা slot এ যা আছে | নাম, দাম, stock | কিছু না: শুধু data |
Machine | একমাত্র প্রবেশপথ, আর সব টাকা আর stock এর মালিক | slot, coin bank, ঢোকানো coin, এখনকার state, lock | Insert, Choose, Cancel |
state (interface) | Machine এই মুহূর্তে event এর উত্তর কীভাবে দেয় | কিছু না | insert, choose, cancel |
idle, hasMoney | দুইটা state | কিছু না | প্রতিটা event এর নিজস্ব উত্তর |
makeChange | একটা coin bank থেকে বড়টা আগে নিয়ে একটা পরিমাণ মেটায় | কিছু না | coin, নতুন bank আর ok ফেরত দেয় |
Sale | Customer যা পায় | product, ভাংতি | কিছু না: একটা value |
কে কার সাথে কীভাবে যুক্ত: Machine তার slot, coin bank আর ঢোকানো coin এর মালিক। সে একটা এখনকার state ধরে রাখে আর প্রতিটা event তার হাতে তুলে দেয়। State গুলো machine কে বদলায় (আর পরের state দিয়ে নিজেদের বদলে দেয়)। makeChange একটা pure function: bank এর copy পায় আর নতুন একটা ফেরত দেয়।
Diagram
Class diagram
কীভাবে পড়বে: Machine এর ভেতরে state type এর একটা field আছে, আর idle আর hasMoney দুটোই ওই interface implement করে। প্রতিটা public button (Insert, Choose, Cancel) শুধু এখনকার state এর কাছে কাজটা পাঠিয়ে দেয়, তাই কোথাও state নিয়ে switch নেই। Product গুলোর মালিক Machine, আর Choose একটা Sale value ফেরত দেয়।
চিহ্ন গুলো: + মানে public, - মানে private। ভরা diamond ◆ মানে মালিক (অংশটা মালিকের সাথেই আসে, সাথেই যায়), সাধারণ তীর মানে reference ধরে রাখে, dashed তীর মানে ব্যবহার করে, আর ফাঁপা ত্রিভুজওয়ালা dashed তীর মানে এই interface implement করে।
Sequence: chips কেনা
কীভাবে পড়বে: প্রতিটা খাড়া dashed লাইন একটা object, আর সময় চলে উপর থেকে নিচে। ভরা তীর মানে call, dashed তীর মানে উত্তর ফেরত আসা, আর সরু bar গুলো দেখায় কোন object কখন কাজে ব্যস্ত। Lock টা Machine এ একবারই নেওয়া হয়, উত্তর ফেরা পর্যন্ত ধরে রাখা থাকে। আসল কাজ করে hasMoney, আর machine কে আবার idle এ নিয়ে যায়ও সে। makeChange যদি বলত "হবে না", তাহলে ফেরার তীরটা হতো একটা error, আর কিছুই বদলাত না।
State: প্রতিটা event কী করে
কীভাবে পড়বে: দুইটা box হলো দুইটা state; তীর গুলো event। Idle এ insert নিয়ে যায় HasMoney তে, আর select বা cancel ফিরিয়ে দেওয়া হয়। HasMoney তে insert এ সেখানেই থাকে, select হয় সেখানেই রাখে (টাকা কম, শেষ হয়ে গেছে), নয়তো বিক্রি করে Idle এ ফেরে, আর cancel টাকা ফেরত দিয়ে Idle এ ফেরে। প্রতিটা state একটা class, যে একই তিনটা event এর উত্তর নিজের মতো দেয়।
Coin bank থেকে ভাংতি দেওয়া
কীভাবে পড়বে: প্রতিটা box একটা coin এর মাপ, আর machine এ কয়টা আছে। ভাংতি দেওয়া হয় এখনো খাপ খায় এমন সবচেয়ে বড় coin নিয়ে। পরিমাণ শূন্যে না নামলে কিছুই নেওয়া হয় না: বিক্রি বাতিল, আর ঢোকানো coin গুলো ফেরত।
Design principle
- Single responsibility:
Machinedata আর lock রাখে; প্রতিটা state একটা পরিস্থিতির rule রাখে;makeChangeশুধু হিসাব করে। - Open/closed: একটা "maintenance" state (সব ফিরিয়ে দেয়, technician এর চাবি নেয়) যোগ করা মানে নতুন একটা type;
Insert,ChooseআরCancelবদলায় না। - Encapsulation: coin, stock আর state private। শুধু তিনটা public method এগুলো বদলাতে পারে, আর তিনটাই lock নেয়।
- হয় সব, নয়তো কিছুই না: একটা বিক্রি হয় পুরোটা হয় (stock কমে, bank update, ভাংতি দেওয়া), নয়তো কিছুই বদলায় না।
makeChangecopy নিয়ে কাজ করে বলে এটা সহজ।
Design pattern
State
- কেন দরকার: select, insert আর cancel এর মানে টাকা থাকলে আর না থাকলে আলাদা।
- কী সমাধান করে: প্রতিটা method এর
if m.status == "idle" { … } else if …সরিয়ে প্রতিটা state এর জন্য একটা object। - না থাকলে কী হতো: প্রতিটা নতুন state (maintenance, card payment, out of order) এ প্রতিটা method edit করতে হতো।
- দাম কী: type বেশি। দুইটা state এর জন্য একটা switch ই যথেষ্ট; state বাড়লে pattern টা নিজের খরচ তুলে নেয়, আর interviewer ঠিক এই follow-up টাই জিজ্ঞেস করেন।
- কোথায় আছে:
state.go।Machineরাখেstate, আর ডাকেm.state.choose(m, slot)।
যে pattern গুলো নেইনি
- ভাংতি দেওয়ার জন্য Strategy: ১০০/৫০/২০/১০/৫ এর জন্য greedy ঠিক কাজ করে। Machine এ কখনো অদ্ভুত মাপের coin আসলে
makeChangeকে একটা dynamic-programming version দিয়ে বদলে দাও; এটা একটাই function, তাই এখনো interface লাগে না। - Machine এর জন্য Singleton: প্রতিটা আসল machine হলো
mainএ বানানো একটা object; test নিজের গুলো বানায়।
Go তে পুরো solution
Project structure
vending/
├── go.mod
├── product.go Product, Sale, accepted coins
├── change.go makeChange: pay an amount from a coin bank
├── state.go the State pattern: idle and hasMoney
├── machine.go Machine: the entry point and the lock
├── machine_test.go tests, including a concurrency test
└── cmd/demo/main.go a customer at the machine
ধাপ ১: product আর coin
// product.go
package vending
// Product sits in one slot. Price is in taka.
type Product struct {
Name string
Price int
Stock int
}
// Coins the machine accepts, largest first (also the order change is given in).
var Denominations = []int{100, 50, 20, 10, 5}
func validCoin(c int) bool {
for _, d := range Denominations {
if d == c {
return true
}
}
return false
}
// Sale is what the customer gets back from a successful purchase.
type Sale struct {
Product string
Change []int
}
ধাপ ২: ভাংতি দেওয়া, copy এর উপর
// change.go
package vending
// makeChange pays `amount` from the coins in `bank`, largest first, and returns the coins
// used. It works on a copy, so a failed attempt changes nothing. Greedy is correct for
// 100/50/20/10/5; for odd coin sets you'd need dynamic programming.
func makeChange(amount int, bank map[int]int) ([]int, map[int]int, bool) {
left := map[int]int{}
for d, n := range bank {
left[d] = n
}
var coins []int
for _, d := range Denominations {
for amount >= d && left[d] > 0 {
amount -= d
left[d]--
coins = append(coins, d)
}
}
return coins, left, amount == 0
}
ধাপ ৩: state গুলো
hasMoney.choose ভাংতি হিসাব করার আগে ঢোকানো coin গুলো bank এর একটা copy তে যোগ করে, যাতে customer এর নিজের coin ও ভাংতিতে ব্যবহার করা যায়। ভাংতি সফল হলে তবেই copy টা আসল bank এর জায়গা নেয়।
// state.go
package vending
import (
"errors"
"fmt"
)
var (
ErrBadCoin = errors.New("not a valid coin")
ErrInsertFirst = errors.New("insert money first")
ErrNothing = errors.New("nothing to refund")
ErrUnknownSlot = errors.New("no such slot")
)
// state is the State pattern: each state answers the same three events in its own way.
type state interface {
insert(m *Machine, coin int) error
choose(m *Machine, slot string) (Sale, error)
cancel(m *Machine) ([]int, error)
}
// idle: no money inserted.
type idle struct{}
func (idle) insert(m *Machine, coin int) error {
m.inserted = append(m.inserted, coin)
m.state = hasMoney{}
return nil
}
func (idle) choose(*Machine, string) (Sale, error) { return Sale{}, ErrInsertFirst }
func (idle) cancel(*Machine) ([]int, error) { return nil, ErrNothing }
// hasMoney: the customer has a balance and can buy or cancel.
type hasMoney struct{}
func (hasMoney) insert(m *Machine, coin int) error {
m.inserted = append(m.inserted, coin)
return nil
}
func (hasMoney) choose(m *Machine, slot string) (Sale, error) {
p, ok := m.slots[slot]
if !ok {
return Sale{}, ErrUnknownSlot
}
if p.Stock == 0 {
return Sale{}, fmt.Errorf("%s is sold out", p.Name)
}
if bal := m.balance(); bal < p.Price {
return Sale{}, fmt.Errorf("%s costs %d tk, insert %d tk more", p.Name, p.Price, p.Price-bal)
}
// The inserted coins join the bank first, so they can be used for change.
bank := map[int]int{}
for d, n := range m.bank {
bank[d] = n
}
for _, c := range m.inserted {
bank[c]++
}
change, after, ok := makeChange(m.balance()-p.Price, bank)
if !ok {
refund := m.inserted
m.inserted, m.state = nil, idle{}
return Sale{}, fmt.Errorf("cannot make change, refunded %v", refund)
}
m.bank = after
p.Stock--
m.inserted, m.state = nil, idle{}
return Sale{Product: p.Name, Change: change}, nil
}
func (hasMoney) cancel(m *Machine) ([]int, error) {
refund := m.inserted
m.inserted, m.state = nil, idle{}
return refund, nil
}
ধাপ ৪: machine
প্রতিটা public method lock নেয়, আর event টা এখনকার state এর হাতে দেয়।
// machine.go
package vending
import "sync"
// Machine is the one entry point. The current state object decides how each event is handled.
type Machine struct {
mu sync.Mutex
state state
slots map[string]*Product
bank map[int]int // coins available for change
inserted []int // this customer's coins, returned exactly on cancel
}
func NewMachine(slots map[string]*Product, bank map[int]int) *Machine {
return &Machine{state: idle{}, slots: slots, bank: bank}
}
func (m *Machine) Insert(coin int) error {
m.mu.Lock()
defer m.mu.Unlock()
if !validCoin(coin) {
return ErrBadCoin
}
return m.state.insert(m, coin)
}
func (m *Machine) Choose(slot string) (Sale, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.state.choose(m, slot)
}
func (m *Machine) Cancel() ([]int, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.state.cancel(m)
}
// Balance is the customer's money in the machine.
func (m *Machine) Balance() int {
m.mu.Lock()
defer m.mu.Unlock()
return m.balance()
}
// balance is for code that already holds the lock (the states).
func (m *Machine) balance() int {
total := 0
for _, c := range m.inserted {
total += c
}
return total
}
// Stock reports how many are left in a slot.
func (m *Machine) Stock(slot string) int {
m.mu.Lock()
defer m.mu.Unlock()
return m.slots[slot].Stock
}
ধাপ ৫: চালিয়ে দেখো
// cmd/demo/main.go
package main
import (
"fmt"
"vending"
)
func main() {
m := vending.NewMachine(
map[string]*vending.Product{
"A1": {Name: "Chips", Price: 30, Stock: 2},
"B2": {Name: "Juice", Price: 65, Stock: 3},
"C3": {Name: "Water", Price: 20, Stock: 0},
},
map[int]int{20: 2, 10: 2, 5: 2}, // coins loaded for change
)
show := func(action, result string) { fmt.Printf("%-14s -> %s\n", action, result) }
insert := func(c int) {
if err := m.Insert(c); err != nil {
show(fmt.Sprint("insert ", c), "rejected: "+err.Error())
return
}
show(fmt.Sprint("insert ", c), fmt.Sprintf("balance %d tk", m.Balance()))
}
choose := func(slot string) {
s, err := m.Choose(slot)
if err != nil {
show("select "+slot, err.Error())
return
}
total := 0
for _, c := range s.Change {
total += c
}
show("select "+slot, fmt.Sprintf("%s, change %d tk %v", s.Product, total, s.Change))
}
insert(20)
choose("A1")
insert(20)
choose("A1")
choose("B2")
insert(100)
choose("B2")
insert(50)
choose("C3")
if r, err := m.Cancel(); err == nil {
show("cancel", fmt.Sprintf("refunded %v", r))
}
insert(3)
insert(100)
choose("A1")
fmt.Printf("stock: A1 %d, B2 %d, C3 %d\n", m.Stock("A1"), m.Stock("B2"), m.Stock("C3"))
}
$ go run ./cmd/demo
insert 20 -> balance 20 tk
select A1 -> Chips costs 30 tk, insert 10 tk more
insert 20 -> balance 40 tk
select A1 -> Chips, change 10 tk [10]
select B2 -> insert money first
insert 100 -> balance 100 tk
select B2 -> Juice, change 35 tk [20 10 5]
insert 50 -> balance 50 tk
select C3 -> Water is sold out
cancel -> refunded [50]
insert 3 -> rejected: not a valid coin
insert 100 -> balance 100 tk
select A1 -> cannot make change, refunded [100]
stock: A1 1, B2 2, C3 0
Output টা পড়ি:
- ২০, তারপর select A1: chips এর দাম ৩০, তাই machine আরো ১০ চায়, আর ২০ রেখে দেয়।
- আরেকটা ২০, select A1: balance ৪০, chips ৩০, ভাংতি ১০।
- টাকা ছাড়া select B2: Idle ফিরিয়ে দেয় "insert money first" বলে।
- ১০০, select B2: juice ৬৫, ৩৫ টাকার ভাংতি আসে ২০ + ১০ + ৫ হিসেবে।
- ৫০, select C3: পানি শেষ; ৫০ ভেতরেই থাকে, আর cancel হুবহু
[50]ফেরত দেয়। - insert 3: এমন coin machine নেয় না।
- ১০০, select A1: ৭০ টাকা ভাংতি লাগবে, কিন্তু machine এ বাকি আছে তিনটা ২০ আর একটা ৫ (৬৫ টাকা)। সে ১০০ ফেরত দেয়, কিছুই বিক্রি করে না। শেষ stock এর লাইন দেখায় A1 এ এখনো ১টা আছে।
বাকি পাঁচটা language
একই design, একই গল্প, একই output। প্রতিটা program চালানো হয়েছে।
from __future__ import annotations
import threading
from dataclasses import dataclass
DENOMINATIONS = [100, 50, 20, 10, 5] # largest first
@dataclass
class Product:
name: str
price: int
stock: int
class VendingError(Exception):
pass
def make_change(amount: int, bank: dict[int, int]) -> tuple[list[int], dict[int, int], bool]:
"""Pay `amount` from `bank`, largest coin first, working on a copy."""
left, coins = dict(bank), []
for d in DENOMINATIONS:
while amount >= d and left.get(d, 0) > 0:
amount -= d
left[d] -= 1
coins.append(d)
return coins, left, amount == 0
class Idle:
"""State: no money inserted."""
def insert(self, m: Machine, coin: int) -> None:
m.inserted.append(coin)
m.state = HasMoney()
def choose(self, m: Machine, slot: str):
raise VendingError("insert money first")
def cancel(self, m: Machine) -> list[int]:
raise VendingError("nothing to refund")
class HasMoney:
"""State: the customer has a balance and can buy or cancel."""
def insert(self, m: Machine, coin: int) -> None:
m.inserted.append(coin)
def choose(self, m: Machine, slot: str) -> tuple[str, list[int]]:
p = m.slots.get(slot)
if p is None:
raise VendingError("no such slot")
if p.stock == 0:
raise VendingError(f"{p.name} is sold out")
bal = sum(m.inserted)
if bal < p.price:
raise VendingError(f"{p.name} costs {p.price} tk, insert {p.price - bal} tk more")
bank = dict(m.bank)
for c in m.inserted: # inserted coins can be used for change
bank[c] = bank.get(c, 0) + 1
change, after, ok = make_change(bal - p.price, bank)
refund = m.inserted
m.inserted, m.state = [], Idle()
if not ok:
raise VendingError(f"cannot make change, refunded {fmt(refund)}")
m.bank = after
p.stock -= 1
return p.name, change
def cancel(self, m: Machine) -> list[int]:
refund = m.inserted
m.inserted, m.state = [], Idle()
return refund
class Machine:
def __init__(self, slots: dict[str, Product], bank: dict[int, int]):
self._lock = threading.Lock()
self.state: Idle | HasMoney = Idle()
self.slots, self.bank = slots, bank
self.inserted: list[int] = []
def insert(self, coin: int) -> None:
with self._lock:
if coin not in DENOMINATIONS:
raise VendingError("not a valid coin")
self.state.insert(self, coin)
def choose(self, slot: str) -> tuple[str, list[int]]:
with self._lock:
return self.state.choose(self, slot)
def cancel(self) -> list[int]:
with self._lock:
return self.state.cancel(self)
def balance(self) -> int:
with self._lock:
return sum(self.inserted)
def fmt(coins: list[int]) -> str:
return "[" + " ".join(map(str, coins)) + "]" # same format as Go
m = Machine({"A1": Product("Chips", 30, 2), "B2": Product("Juice", 65, 3), "C3": Product("Water", 20, 0)},
{20: 2, 10: 2, 5: 2})
def show(action: str, result: str) -> None:
print(f"{action:<14} -> {result}")
def insert(c: int) -> None:
try:
m.insert(c)
show(f"insert {c}", f"balance {m.balance()} tk")
except VendingError as e:
show(f"insert {c}", f"rejected: {e}")
def choose(slot: str) -> None:
try:
name, change = m.choose(slot)
show(f"select {slot}", f"{name}, change {sum(change)} tk {fmt(change)}")
except VendingError as e:
show(f"select {slot}", str(e))
insert(20)
choose("A1")
insert(20)
choose("A1")
choose("B2")
insert(100)
choose("B2")
insert(50)
choose("C3")
show("cancel", f"refunded {fmt(m.cancel())}")
insert(3)
insert(100)
choose("A1")
print(f"stock: A1 {m.slots['A1'].stock}, B2 {m.slots['B2'].stock}, C3 {m.slots['C3'].stock}")const DENOMINATIONS = [100, 50, 20, 10, 5]; // largest first
interface Product {
readonly name: string;
readonly price: number;
stock: number;
}
type Bank = Map<number, number>;
// Pay `amount` from `bank`, largest coin first, working on a copy.
function makeChange(amount: number, bank: Bank): [number[], Bank, boolean] {
const left = new Map(bank);
const coins: number[] = [];
for (const d of DENOMINATIONS) {
while (amount >= d && (left.get(d) ?? 0) > 0) {
amount -= d;
left.set(d, left.get(d)! - 1);
coins.push(d);
}
}
return [coins, left, amount === 0];
}
const fmt = (coins: number[]) => `[${coins.join(" ")}]`; // same format as Go
// The State pattern: each state answers the same three events in its own way.
interface State {
insert(m: Machine, coin: number): void;
choose(m: Machine, slot: string): { product: string; change: number[] };
cancel(m: Machine): number[];
}
const idle: State = {
insert(m, coin) {
m.inserted.push(coin);
m.state = hasMoney;
},
choose() {
throw new Error("insert money first");
},
cancel() {
throw new Error("nothing to refund");
},
};
const hasMoney: State = {
insert(m, coin) {
m.inserted.push(coin);
},
choose(m, slot) {
const p = m.slots.get(slot);
if (!p) throw new Error("no such slot");
if (p.stock === 0) throw new Error(`${p.name} is sold out`);
const bal = m.balance();
if (bal < p.price) throw new Error(`${p.name} costs ${p.price} tk, insert ${p.price - bal} tk more`);
const bank = new Map(m.bank);
for (const c of m.inserted) bank.set(c, (bank.get(c) ?? 0) + 1); // inserted coins can be used for change
const [change, after, ok] = makeChange(bal - p.price, bank);
const refund = m.inserted;
m.inserted = [];
m.state = idle;
if (!ok) throw new Error(`cannot make change, refunded ${fmt(refund)}`);
m.bank = after;
p.stock--;
return { product: p.name, change };
},
cancel(m) {
const refund = m.inserted;
m.inserted = [];
m.state = idle;
return refund;
},
};
// No lock: JavaScript runs this on one thread and nothing here awaits.
class Machine {
state: State = idle;
inserted: number[] = [];
readonly slots: Map<string, Product>;
bank: Bank;
constructor(slots: Map<string, Product>, bank: Bank) {
this.slots = slots;
this.bank = bank;
}
insert(coin: number): void {
if (!DENOMINATIONS.includes(coin)) throw new Error("not a valid coin");
this.state.insert(this, coin);
}
choose(slot: string) {
return this.state.choose(this, slot);
}
cancel(): number[] {
return this.state.cancel(this);
}
balance(): number {
return this.inserted.reduce((a, b) => a + b, 0);
}
}
const m = new Machine(
new Map([
["A1", { name: "Chips", price: 30, stock: 2 }],
["B2", { name: "Juice", price: 65, stock: 3 }],
["C3", { name: "Water", price: 20, stock: 0 }],
]),
new Map([[20, 2], [10, 2], [5, 2]]),
);
const show = (action: string, result: string) => console.log(`${action.padEnd(14)} -> ${result}`);
function insert(c: number): void {
try {
m.insert(c);
show(`insert ${c}`, `balance ${m.balance()} tk`);
} catch (e) {
show(`insert ${c}`, `rejected: ${(e as Error).message}`);
}
}
function choose(slot: string): void {
try {
const s = m.choose(slot);
show(`select ${slot}`, `${s.product}, change ${s.change.reduce((a, b) => a + b, 0)} tk ${fmt(s.change)}`);
} catch (e) {
show(`select ${slot}`, (e as Error).message);
}
}
insert(20);
choose("A1");
insert(20);
choose("A1");
choose("B2");
insert(100);
choose("B2");
insert(50);
choose("C3");
show("cancel", `refunded ${fmt(m.cancel())}`);
insert(3);
insert(100);
choose("A1");
const st = (s: string) => m.slots.get(s)!.stock;
console.log(`stock: A1 ${st("A1")}, B2 ${st("B2")}, C3 ${st("C3")}`);use std::collections::HashMap;
use std::sync::Mutex;
const DENOMINATIONS: [u32; 5] = [100, 50, 20, 10, 5]; // largest first
struct Product { name: &'static str, price: u32, stock: u32 }
// Pay `amount` from `bank`, largest coin first, working on a copy.
fn make_change(mut amount: u32, bank: &HashMap<u32, u32>) -> (Vec<u32>, HashMap<u32, u32>, bool) {
let mut left = bank.clone();
let mut coins = vec![];
for d in DENOMINATIONS {
while amount >= d && left.get(&d).copied().unwrap_or(0) > 0 {
amount -= d;
*left.get_mut(&d).unwrap() -= 1;
coins.push(d);
}
}
(coins, left, amount == 0)
}
fn fmt(coins: &[u32]) -> String { // same format as Go
format!("[{}]", coins.iter().map(|c| c.to_string()).collect::<Vec<_>>().join(" "))
}
// The states as an enum; each event is a match on the current state.
#[derive(Clone, Copy, PartialEq)]
enum State { Idle, HasMoney }
struct Inner { state: State, slots: HashMap<&'static str, Product>, bank: HashMap<u32, u32>, inserted: Vec<u32> }
struct Machine { inner: Mutex<Inner> } // everything that changes lives inside the lock
impl Machine {
fn insert(&self, coin: u32) -> Result<(), String> {
if !DENOMINATIONS.contains(&coin) { return Err("not a valid coin".into()); }
let mut m = self.inner.lock().unwrap();
m.inserted.push(coin);
m.state = State::HasMoney;
Ok(())
}
fn choose(&self, slot: &str) -> Result<(&'static str, Vec<u32>), String> {
let mut guard = self.inner.lock().unwrap();
let m = &mut *guard;
if m.state == State::Idle { return Err("insert money first".into()); }
let bal: u32 = m.inserted.iter().sum();
let p = m.slots.get(slot).ok_or("no such slot")?;
if p.stock == 0 { return Err(format!("{} is sold out", p.name)); }
if bal < p.price { return Err(format!("{} costs {} tk, insert {} tk more", p.name, p.price, p.price - bal)); }
let (name, price) = (p.name, p.price);
let mut bank = m.bank.clone();
for &c in &m.inserted { *bank.entry(c).or_insert(0) += 1; } // inserted coins can be used for change
let (change, after, ok) = make_change(bal - price, &bank);
let refund = std::mem::take(&mut m.inserted);
m.state = State::Idle;
if !ok { return Err(format!("cannot make change, refunded {}", fmt(&refund))); }
m.bank = after;
m.slots.get_mut(slot).unwrap().stock -= 1;
Ok((name, change))
}
fn cancel(&self) -> Result<Vec<u32>, String> {
let mut m = self.inner.lock().unwrap();
if m.state == State::Idle { return Err("nothing to refund".into()); }
m.state = State::Idle;
Ok(std::mem::take(&mut m.inserted))
}
fn balance(&self) -> u32 { self.inner.lock().unwrap().inserted.iter().sum() }
fn stock(&self, slot: &str) -> u32 { self.inner.lock().unwrap().slots[slot].stock }
}
fn main() {
let slots = HashMap::from([
("A1", Product { name: "Chips", price: 30, stock: 2 }),
("B2", Product { name: "Juice", price: 65, stock: 3 }),
("C3", Product { name: "Water", price: 20, stock: 0 }),
]);
let m = Machine { inner: Mutex::new(Inner { state: State::Idle, slots, bank: HashMap::from([(20, 2), (10, 2), (5, 2)]), inserted: vec![] }) };
let show = |action: &str, result: &str| println!("{action:<14} -> {result}");
let insert = |c: u32| match m.insert(c) {
Ok(()) => show(&format!("insert {c}"), &format!("balance {} tk", m.balance())),
Err(e) => show(&format!("insert {c}"), &format!("rejected: {e}")),
};
let choose = |slot: &str| match m.choose(slot) {
Ok((name, change)) => show(&format!("select {slot}"), &format!("{name}, change {} tk {}", change.iter().sum::<u32>(), fmt(&change))),
Err(e) => show(&format!("select {slot}"), &e),
};
insert(20);
choose("A1");
insert(20);
choose("A1");
choose("B2");
insert(100);
choose("B2");
insert(50);
choose("C3");
if let Ok(r) = m.cancel() { show("cancel", &format!("refunded {}", fmt(&r))); }
insert(3);
insert(100);
choose("A1");
println!("stock: A1 {}, B2 {}, C3 {}", m.stock("A1"), m.stock("B2"), m.stock("C3"));
}import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Main {
static final int[] DENOMINATIONS = {100, 50, 20, 10, 5}; // largest first
static final class Product {
final String name;
final int price;
int stock;
Product(String name, int price, int stock) { this.name = name; this.price = price; this.stock = stock; }
}
static final class VendingException extends Exception {
private static final long serialVersionUID = 1L;
VendingException(String msg) { super(msg); }
}
record Sale(String product, List<Integer> change) {}
record Change(List<Integer> coins, Map<Integer, Integer> left, boolean ok) {}
// Pay `amount` from `bank`, largest coin first, working on a copy.
static Change makeChange(int amount, Map<Integer, Integer> bank) {
Map<Integer, Integer> left = new HashMap<>(bank);
List<Integer> coins = new ArrayList<>();
for (int d : DENOMINATIONS) {
while (amount >= d && left.getOrDefault(d, 0) > 0) {
amount -= d;
left.merge(d, -1, Integer::sum);
coins.add(d);
}
}
return new Change(coins, left, amount == 0);
}
static String fmt(List<Integer> coins) { // same format as Go
return coins.stream().map(String::valueOf).collect(Collectors.joining(" ", "[", "]"));
}
// The State pattern: each state answers the same three events in its own way.
interface State {
void insert(Machine m, int coin);
Sale choose(Machine m, String slot) throws VendingException;
List<Integer> cancel(Machine m) throws VendingException;
}
static final class Idle implements State {
public void insert(Machine m, int coin) { m.inserted.add(coin); m.state = new HasMoney(); }
public Sale choose(Machine m, String slot) throws VendingException { throw new VendingException("insert money first"); }
public List<Integer> cancel(Machine m) throws VendingException { throw new VendingException("nothing to refund"); }
}
static final class HasMoney implements State {
public void insert(Machine m, int coin) { m.inserted.add(coin); }
public Sale choose(Machine m, String slot) throws VendingException {
Product p = m.slots.get(slot);
if (p == null) throw new VendingException("no such slot");
if (p.stock == 0) throw new VendingException(p.name + " is sold out");
int bal = m.balanceUnlocked();
if (bal < p.price) throw new VendingException(p.name + " costs " + p.price + " tk, insert " + (p.price - bal) + " tk more");
Map<Integer, Integer> bank = new HashMap<>(m.bank);
for (int c : m.inserted) bank.merge(c, 1, Integer::sum); // inserted coins can be used for change
Change ch = makeChange(bal - p.price, bank);
List<Integer> refund = m.inserted;
m.inserted = new ArrayList<>();
m.state = new Idle();
if (!ch.ok()) throw new VendingException("cannot make change, refunded " + fmt(refund));
m.bank = ch.left();
p.stock--;
return new Sale(p.name, ch.coins());
}
public List<Integer> cancel(Machine m) {
List<Integer> refund = m.inserted;
m.inserted = new ArrayList<>();
m.state = new Idle();
return refund;
}
}
static final class Machine {
State state = new Idle();
final Map<String, Product> slots;
Map<Integer, Integer> bank;
List<Integer> inserted = new ArrayList<>();
Machine(Map<String, Product> slots, Map<Integer, Integer> bank) { this.slots = slots; this.bank = bank; }
synchronized void insert(int coin) throws VendingException {
boolean valid = false;
for (int d : DENOMINATIONS) valid |= d == coin;
if (!valid) throw new VendingException("not a valid coin");
state.insert(this, coin);
}
synchronized Sale choose(String slot) throws VendingException { return state.choose(this, slot); }
synchronized List<Integer> cancel() throws VendingException { return state.cancel(this); }
synchronized int balance() { return balanceUnlocked(); }
int balanceUnlocked() { return inserted.stream().mapToInt(Integer::intValue).sum(); }
}
static Machine m;
static void show(String action, String result) { System.out.printf("%-14s -> %s%n", action, result); }
static void insert(int c) {
try {
m.insert(c);
show("insert " + c, "balance " + m.balance() + " tk");
} catch (VendingException e) {
show("insert " + c, "rejected: " + e.getMessage());
}
}
static void choose(String slot) {
try {
Sale s = m.choose(slot);
int total = s.change().stream().mapToInt(Integer::intValue).sum();
show("select " + slot, s.product() + ", change " + total + " tk " + fmt(s.change()));
} catch (VendingException e) {
show("select " + slot, e.getMessage());
}
}
public static void main(String[] args) throws VendingException {
Map<String, Product> slots = new HashMap<>();
slots.put("A1", new Product("Chips", 30, 2));
slots.put("B2", new Product("Juice", 65, 3));
slots.put("C3", new Product("Water", 20, 0));
m = new Machine(slots, new HashMap<>(Map.of(20, 2, 10, 2, 5, 2)));
insert(20);
choose("A1");
insert(20);
choose("A1");
choose("B2");
insert(100);
choose("B2");
insert(50);
choose("C3");
show("cancel", "refunded " + fmt(m.cancel()));
insert(3);
insert(100);
choose("A1");
System.out.printf("stock: A1 %d, B2 %d, C3 %d%n", slots.get("A1").stock, slots.get("B2").stock, slots.get("C3").stock);
}
}#include <cstdio>
#include <map>
#include <memory>
#include <mutex>
#include <numeric>
#include <stdexcept>
#include <string>
#include <vector>
const std::vector<int> kDenominations{100, 50, 20, 10, 5}; // largest first
struct Product {
std::string name;
int price, stock;
};
struct VendingError : std::runtime_error {
using std::runtime_error::runtime_error;
};
std::string fmt(const std::vector<int>& coins) { // same format as Go
std::string s = "[";
for (size_t i = 0; i < coins.size(); i++) s += (i ? " " : "") + std::to_string(coins[i]);
return s + "]";
}
// Pay `amount` from `bank`, largest coin first, working on a copy.
bool makeChange(int amount, std::map<int, int>& bank, std::vector<int>& coins) {
for (int d : kDenominations)
while (amount >= d && bank[d] > 0) {
amount -= d;
bank[d]--;
coins.push_back(d);
}
return amount == 0;
}
struct Sale {
std::string product;
std::vector<int> change;
};
class Machine;
// The State pattern: each state answers the same three events in its own way.
class State {
public:
virtual ~State() = default;
virtual void insert(Machine& m, int coin) = 0;
virtual Sale choose(Machine& m, const std::string& slot) = 0;
virtual std::vector<int> cancel(Machine& m) = 0;
};
class Machine {
public:
Machine(std::map<std::string, Product> slots, std::map<int, int> bank);
void insert(int coin);
Sale choose(const std::string& slot);
std::vector<int> cancel();
int balance();
int stock(const std::string& slot) {
std::lock_guard lock(mu_);
return slots.at(slot).stock;
}
// Used by the states, always under the lock.
std::unique_ptr<State> state;
std::map<std::string, Product> slots;
std::map<int, int> bank;
std::vector<int> inserted;
int balanceUnlocked() const { return std::accumulate(inserted.begin(), inserted.end(), 0); }
private:
std::mutex mu_;
};
class Idle : public State {
public:
void insert(Machine& m, int coin) override;
Sale choose(Machine&, const std::string&) override { throw VendingError("insert money first"); }
std::vector<int> cancel(Machine&) override { throw VendingError("nothing to refund"); }
};
class HasMoney : public State {
public:
void insert(Machine& m, int coin) override { m.inserted.push_back(coin); }
Sale choose(Machine& m, const std::string& slot) override {
auto it = m.slots.find(slot);
if (it == m.slots.end()) throw VendingError("no such slot");
Product& p = it->second;
if (p.stock == 0) throw VendingError(p.name + " is sold out");
int bal = m.balanceUnlocked();
if (bal < p.price)
throw VendingError(p.name + " costs " + std::to_string(p.price) + " tk, insert " + std::to_string(p.price - bal) + " tk more");
std::map<int, int> bank = m.bank;
for (int c : m.inserted) bank[c]++; // inserted coins can be used for change
std::vector<int> change;
bool ok = makeChange(bal - p.price, bank, change);
std::vector<int> refund = std::move(m.inserted);
m.inserted.clear();
m.state = std::make_unique<Idle>(); // `this` is destroyed here: touch only locals from now on
if (!ok) throw VendingError("cannot make change, refunded " + fmt(refund));
m.bank = bank;
p.stock--;
return {p.name, change};
}
std::vector<int> cancel(Machine& m) override {
std::vector<int> refund = std::move(m.inserted);
m.inserted.clear();
m.state = std::make_unique<Idle>();
return refund;
}
};
void Idle::insert(Machine& m, int coin) {
m.inserted.push_back(coin);
m.state = std::make_unique<HasMoney>(); // replaces (destroys) this Idle object; return right away
}
Machine::Machine(std::map<std::string, Product> s, std::map<int, int> b)
: state(std::make_unique<Idle>()), slots(std::move(s)), bank(std::move(b)) {}
void Machine::insert(int coin) {
std::lock_guard lock(mu_);
bool valid = false;
for (int d : kDenominations) valid |= d == coin;
if (!valid) throw VendingError("not a valid coin");
state->insert(*this, coin);
}
Sale Machine::choose(const std::string& slot) {
std::lock_guard lock(mu_);
return state->choose(*this, slot);
}
std::vector<int> Machine::cancel() {
std::lock_guard lock(mu_);
return state->cancel(*this);
}
int Machine::balance() {
std::lock_guard lock(mu_);
return balanceUnlocked();
}
int main() {
Machine m({{"A1", {"Chips", 30, 2}}, {"B2", {"Juice", 65, 3}}, {"C3", {"Water", 20, 0}}}, {{20, 2}, {10, 2}, {5, 2}});
auto show = [](const std::string& action, const std::string& result) {
std::printf("%-14s -> %s\n", action.c_str(), result.c_str());
};
auto insert = [&](int c) {
try {
m.insert(c);
show("insert " + std::to_string(c), "balance " + std::to_string(m.balance()) + " tk");
} catch (const VendingError& e) {
show("insert " + std::to_string(c), std::string("rejected: ") + e.what());
}
};
auto choose = [&](const std::string& slot) {
try {
Sale s = m.choose(slot);
int total = std::accumulate(s.change.begin(), s.change.end(), 0);
show("select " + slot, s.product + ", change " + std::to_string(total) + " tk " + fmt(s.change));
} catch (const VendingError& e) {
show("select " + slot, e.what());
}
};
insert(20);
choose("A1");
insert(20);
choose("A1");
choose("B2");
insert(100);
choose("B2");
insert(50);
choose("C3");
show("cancel", "refunded " + fmt(m.cancel()));
insert(3);
insert(100);
choose("A1");
std::printf("stock: A1 %d, B2 %d, C3 %d\n", m.stock("A1"), m.stock("B2"), m.stock("C3"));
}এক language থেকে আরেকটায় কী বদলায়:
| State গুলো | Lock | |
|---|---|---|
| Go | দুইটা ছোট type সহ একটা interface | প্রতিটা public method এ sync.Mutex |
| Python | একই method ওয়ালা দুইটা class (duck typing) | with সহ threading.Lock |
| TypeScript | State type এর দুইটা object literal | দরকার নেই: এক thread, কোনো await নেই |
| Rust | একটা enum State আর match: প্রতিটা state handle হলো কিনা compiler দেখে | সব data একটা Mutex এর ভেতরে |
| Java | দুইটা class সহ একটা interface | synchronized method |
| C++ | একটা abstract class, unique_ptr<State> | std::lock_guard |
C++ এর একটা খুঁটিনাটি জানা ভালো: কোনো state নিজেকে বদলে দিলে (m.state = make_unique<Idle>()), পুরনো state object ওই লাইনেই ধ্বংস হয়, তাই তার পরে code শুধু local variable ছোঁয়।
Testing
// machine_test.go
package vending
import (
"errors"
"slices"
"sync"
"testing"
)
func newTestMachine() *Machine {
return NewMachine(
map[string]*Product{
"A1": {Name: "Chips", Price: 30, Stock: 2},
"C3": {Name: "Water", Price: 20, Stock: 0},
},
map[int]int{20: 2, 10: 2, 5: 2},
)
}
func TestChooseBeforeInsert(t *testing.T) {
m := newTestMachine()
if _, err := m.Choose("A1"); !errors.Is(err, ErrInsertFirst) {
t.Fatalf("got %v", err)
}
if _, err := m.Cancel(); !errors.Is(err, ErrNothing) {
t.Fatalf("cancel in idle: %v", err)
}
}
func TestBadCoinChangesNothing(t *testing.T) {
m := newTestMachine()
if err := m.Insert(3); !errors.Is(err, ErrBadCoin) {
t.Fatalf("got %v", err)
}
if m.Balance() != 0 {
t.Fatalf("balance %d", m.Balance())
}
}
func TestNotEnoughMoneyKeepsBalance(t *testing.T) {
m := newTestMachine()
m.Insert(20)
if _, err := m.Choose("A1"); err == nil {
t.Fatal("expected an error")
}
if m.Balance() != 20 || m.Stock("A1") != 2 {
t.Fatalf("balance %d stock %d", m.Balance(), m.Stock("A1"))
}
}
func TestExactChange(t *testing.T) {
m := newTestMachine()
m.Insert(50)
s, err := m.Choose("A1")
if err != nil || s.Product != "Chips" || !slices.Equal(s.Change, []int{20}) {
t.Fatalf("got %+v, %v", s, err)
}
if m.Stock("A1") != 1 || m.Balance() != 0 {
t.Fatal("stock or balance not updated")
}
}
func TestSoldOut(t *testing.T) {
m := newTestMachine()
m.Insert(20)
if _, err := m.Choose("C3"); err == nil || err.Error() != "Water is sold out" {
t.Fatalf("got %v", err)
}
if m.Balance() != 20 {
t.Fatal("sold out should keep the money in")
}
}
func TestCancelReturnsSameCoins(t *testing.T) {
m := newTestMachine()
m.Insert(10)
m.Insert(5)
got, err := m.Cancel()
if err != nil || !slices.Equal(got, []int{10, 5}) {
t.Fatalf("got %v, %v", got, err)
}
}
func TestNoChangeRefundsAndKeepsBank(t *testing.T) {
m := NewMachine(map[string]*Product{"A1": {Name: "Chips", Price: 30, Stock: 1}}, map[int]int{})
m.Insert(100)
if _, err := m.Choose("A1"); err == nil {
t.Fatal("expected cannot make change")
}
if m.Stock("A1") != 1 || m.Balance() != 0 || len(m.bank) != 0 {
t.Fatal("a failed sale must change nothing")
}
}
// Many goroutines inserting at once must not lose a coin. Run with -race.
func TestConcurrentInserts(t *testing.T) {
m := newTestMachine()
var wg sync.WaitGroup
for range 50 {
wg.Add(1)
go func() { defer wg.Done(); m.Insert(10) }()
}
wg.Wait()
if m.Balance() != 500 {
t.Fatalf("balance %d, want 500", m.Balance())
}
}
$ go test -race -v ./...
=== RUN TestChooseBeforeInsert
--- PASS: TestChooseBeforeInsert (0.00s)
=== RUN TestBadCoinChangesNothing
--- PASS: TestBadCoinChangesNothing (0.00s)
=== RUN TestNotEnoughMoneyKeepsBalance
--- PASS: TestNotEnoughMoneyKeepsBalance (0.00s)
=== RUN TestExactChange
--- PASS: TestExactChange (0.00s)
=== RUN TestSoldOut
--- PASS: TestSoldOut (0.00s)
=== RUN TestCancelReturnsSameCoins
--- PASS: TestCancelReturnsSameCoins (0.00s)
=== RUN TestNoChangeRefundsAndKeepsBank
--- PASS: TestNoChangeRefundsAndKeepsBank (0.00s)
=== RUN TestConcurrentInserts
--- PASS: TestConcurrentInserts (0.00s)
PASS
ok vending 1.012s
? vending/cmd/demo [no test files]
| Test | যা check করে |
|---|---|
TestChooseBeforeInsert | Idle select আর cancel ফিরিয়ে দেয় |
TestBadCoinChangesNothing | ৩ টাকার coin ফিরিয়ে দেওয়া হয়, balance ০ থাকে |
TestNotEnoughMoneyKeepsBalance | কম টাকায় select করলে টাকা আর stock দুটোই থাকে |
TestExactChange | ৩০ টাকার chips এ ৫০ দিলে একটা ২০ ফেরত |
TestSoldOut | শেষ হয়ে গেলে টাকা ভেতরেই থাকে |
TestCancelReturnsSameCoins | Cancel ফেরত দেয় [10 5], যেগুলো আসলেই ঢোকানো হয়েছিল |
TestNoChangeRefundsAndKeepsBank | Fail করা বিক্রি কিছুই বদলায় না: stock, balance, bank |
TestConcurrentInserts | ৫০টা goroutine প্রত্যেকে ১০ ঢোকায়: balance ঠিক ৫০০ |
Insert থেকে lock সরিয়ে concurrency test আবার চালিয়েছি: race detector ৭টা data race দেখালো, আর test fail করলো। Lock সহ pass করে।
Design review
যা ভালো: প্রতিটা রাস্তা শেষ হয় বিক্রিতে নয়তো পুরো টাকা ফেরতে; State pattern প্রতিটা পরিস্থিতির rule একসাথে রাখে; ভাংতি দেওয়া একটা pure function, test করা সহজ।
| "যদি…" | কী বদলাতে হবে |
|---|---|
| Card বা bKash payment | Coin এর পাশে নতুন একটা payment এর উৎস; select করলে balance না, card থেকে কাটা হবে। সম্ভবত নতুন একটা state, PayingByCard। |
| Maintenance mode | একটা maintenance state, যেটা customer ফিরিয়ে দেয় আর restock করতে দেয়। আগের কোনো method বদলায় না। |
| অদ্ভুত মাপের coin (ধরো ২৫) | Solution থাকলেও greedy fail করতে পারে; makeChange কে dynamic programming দিয়ে বদলাও। |
| ছোট coin রেখে দিতে চাই | ভাংতির নীতি একটা strategy হয়ে যায়: বড় coin আগে দাও, বা machine এ কমপক্ষে কয়েকটা ৫ রেখে দাও। |
| বিক্রির মাঝে কারেন্ট চলে গেল | ঢোকানো coin আর stock save করো; চালু হলে ঢোকানো coin ফেরত দাও। |
Production এ যাওয়ার আগে: প্রতিটা insert, বিক্রি আর refund এর একটা event log (ঝামেলা হলে দেখার জন্য), আর ভাংতি কমে গেলে একটা সতর্কবার্তা, যাতে machine বিক্রি ফিরিয়ে দেওয়া শুরু করার আগেই staff coin ভরে দেয়।
Interview প্রশ্ন
Practice প্রশ্ন
Beginner
- Status এর উপর
switchনা দিয়ে State pattern কেন? - শুধু মোট টাকা না রেখে ঢোকানো coin এর list কেন রাখো?
Intermediate
- Fail করা বিক্রি যেন কিছুই না বদলায়, সেটা কীভাবে নিশ্চিত করো?
- "Maintenance mode" কোথায় যাবে?
Advanced
- Greedy ভাংতি কখন ভুল, আর তার বদলে কী ব্যবহার করবে?
- Touch screen আর coin slot আলাদা thread এ চলে। Lock না থাকলে কী গোলমাল হতে পারে?
Design এর সাধারণ ভুল
- একটা status string, আর প্রতিটা method এ
ifএর chain। - ভাংতি দেওয়া যাবে কিনা জানার আগেই bank থেকে coin নিয়ে নেওয়া, তাতে fail করা বিক্রির পরে bank এ কম থাকে।
- "৫০ টাকা" যেকোনো coin এ ফেরত দেওয়া, customer এর নিজের coin না দিয়ে।
- Payment সফল হওয়ার আগেই stock কমানো।
উত্তর
১. State pattern কেন?
প্রতিটা event প্রতিটা state এ আলাদা আচরণ করে। Switch দিলে প্রতিটা method এ প্রতিটা state এর জন্য একটা branch বাড়ে, আর প্রতিটা নতুন state এ প্রতিটা method edit করতে হয়। Pattern দিলে নতুন state মানে নতুন type, আর machine শুধু কাজটা তার হাতে তুলে দেয়।
২. ঢোকানো coin কেন রাখবো?
Cancel এ customer যা ঢুকিয়েছিল সেটাই ফেরত দেওয়া উচিত, আর ঢোকানো coin দিয়ে ভাংতিও দেওয়া যায়। শুধু মোট রাখলে দুটোই হারায়।
৩. Fail করা বিক্রি কীভাবে কিছুই বদলায় না?
makeChange bank এর copy নিয়ে কাজ করে। ভাংতি সফল হলে তবেই stock আর bank update হয়; fail হলে ঢোকানো coin ফেরত, আর state আবার idle।
৪. Maintenance mode কোথায় যাবে?
নতুন একটা state type এ: সে insert আর select ফিরিয়ে দেয়, আর machine এ এই mode এ ঢোকা আর বের হওয়ার জন্য technician এর একটা method থাকে। আগের state গুলো বদলায় না।
৫. Greedy কখন ভুল?
২৫/১০/১ এর মতো coin এর set এ greedy ৩০ মেটায় ২৫ + ১×৫ দিয়ে (ছয়টা coin), ১০×৩ এর বদলে। আর coin সীমিত থাকলে অন্য একটা combination কাজ করলেও greedy fail করতে পারে। হাতে থাকা coin এর উপর dynamic programming solution থাকলে সেটা খুঁজে পায়।
৬. Lock না থাকলে কী গোলমাল হয়?
Insert এর "ঢোকানো list পড়ো, যোগ করো" আর select এর "balance পড়ো, list খালি করো" মাঝখানে মিশে যেতে পারে, তাতে একটা coin হারায় বা পুরনো balance দিয়ে বিক্রি হয়। Lock সরিয়ে আমাদের test আসল data race দেখিয়েছে।
Interviewer রা যা দেখেন: পরিষ্কার একটা state table, কারণ সহ State pattern, হয় সব নয়তো কিছুই না এমন বিক্রি, আর ভাংতি দেওয়া নিয়ে বুঝেশুনে একটা উত্তর।
নিজে design করে solution টা বুঝেছো? Mark করো, তাহলে progress ট্র্যাক হবে।