題幹與適用場景
這是一道低層設計與編碼題,重點是把預約、桌位、時間區間和候補佇列轉化為職責清晰的物件。公開面經曾出現餐廳預約設計,OOD 方法也強調先釐清範圍、列出需求,再確定物件與介面。題目假設單店、固定營業時間和記憶體內模組;若面試官要求持久化,再補充儲存和並發策略。
面試官考察點
- 能否把
Restaurant、Table、Reservation、Waitlist和分配策略分離。 - 能否正確處理半開時間區間、桌位容量、併桌規則和取消後的釋放。
- 能否設計狀態機、冪等請求和並發下的防重複分配。
- 能否用測試覆蓋邊界條件,並解釋複雜度與可擴展點。
回答前需要釐清的問題
先確認預約是指定桌位還是只指定人數,是否允許併桌;預約時長和清桌緩衝是多少;是否支援修改、遲到、爽約和臨時到店;候補佇列按先到先得還是按人數與時間匹配;一個請求是否可能重試。還要確認時間精度、時區、單店還是多店,以及是否要求跨程序持久化。
30 秒回答框架
我會先定義不可變的 TimeRange 和預約狀態,再讓 AvailabilityService 負責衝突查詢,讓 TableAllocator 負責按容量和策略選桌,ReservationService 負責編排建立、取消和通知,Waitlist 單獨管理候補。建立時用冪等鍵和同一臨界區檢查「可用再佔用」,取消只允許合法狀態轉換並釋放桌位。最後用邊界測試驗證相鄰時段、並發請求、重複取消和候補晉級。
分步驟深入解答
1. 建模時間、桌位和預約狀態
用半開區間 [start, end) 表示預約,只有 start < end 才有效;兩個區間在 a.start < b.end && b.start < a.end 時衝突。Table 保存容量、編號和可用狀態,Reservation 保存顧客、人數、區間、桌位和狀態。狀態可為 HELD、CONFIRMED、SEATED、CANCELLED、NO_SHOW,非法轉換直接拒絕。
2. 讓分配策略可替換
TableAllocator 接收候選桌位和預約需求,預設選擇「容量最小但足夠」的桌,減少大桌浪費;也可以注入「優先連桌」或「無障礙桌優先」策略。分配器不負責寫預約,避免把搜尋、決策和狀態持久化耦合到一個巨大類別中。
3. 實作衝突查詢和建立
查詢按餐廳、時間區間和桌位索引過濾,再由分配器選擇。建立流程先驗證輸入、計算緩衝後的區間、產生冪等鍵,然後在同一鎖或交易邊界內重新讀取可用桌並寫入預約;不能依賴查詢階段的舊結果。核心不變量是同一桌的重疊 CONFIRMED 預約數必須為零。
create(request, key):
if idempotency.exists(key): return idempotency.result(key)
range = TimeRange(request.start, request.end + cleanupBuffer)
lock(restaurantId, range):
table = allocator.choose(availableTables(range), request.partySize)
if table is null: return WAITLISTED
reservation = Reservation.confirm(request, table, range)
store(reservation)
idempotency.save(key, reservation.id)
return reservation4. 處理取消、遲到和候補晉級
取消只允許 HELD 或 CONFIRMED 進入 CANCELLED,重複取消返回同一結果,不重複觸發通知。到店後轉為 SEATED,超過規則時間可轉 NO_SHOW 並釋放桌位。釋放事件交給 WaitlistMatcher,按等待時間、人數適配和優先級挑選候補,再走同一建立臨界區,防止候補與新預約競爭時雙重分配。
5. 測試並說明複雜度
測試相鄰區間 [19:00,20:00) 與 [20:00,21:00) 不衝突,反向輸入被拒絕,緩衝時間會擴大衝突範圍,重複冪等鍵不新增預約,取消後候補最多晉級一次,並發建立只能成功一個。若每張桌按時間排序保存預約,單桌衝突查詢可為 O(log n + k);候選桌為 m 張時,選擇和寫入約為 O(m log n),可再按容量分桶或使用區間索引。
高品質示範回答
我會用半開 TimeRange、明確的預約狀態機和可替換分配策略建模。AvailabilityService 負責按餐廳、時間和桌位過濾,TableAllocator 選擇容量最小但足夠的桌,ReservationService 在同一鎖或交易邊界中重新檢查並寫入,使用冪等鍵防止重試重複建立。取消、遲到和爽約透過合法狀態轉換釋放桌位,候補匹配隨後重用同一臨界區。測試覆蓋相鄰區間、清桌緩衝、重複取消、並發建立和候補競爭,並說明按桌排序索引後的複雜度。
常見錯誤
- 把所有邏輯塞進
Restaurant,物件職責和替換策略不清楚。 - 用閉區間判斷時間,導致相鄰預約被錯誤判為衝突。
- 先查詢可用桌,再非同步寫入,沒有在寫入邊界重新檢查。
- 取消介面不是冪等的,重試會重複釋放桌位或發送通知。
- 只實作顧客預約,忽略前台 walk-in、遲到、爽約和候補。
- 只給類別圖,不寫不變量、邊界測試和複雜度。
追問及應對
如果允許併桌,怎麼擴展?
把分配結果從單個 tableId 改成有序桌位集合,並增加「組合後容量足夠、桌間可連接、組合不與其他預約衝突」的約束。分配器介面保持不變,新增 ComposableTableAllocator。
跨程序部署時如何防止重複預訂?
把衝突約束下沉到持久化層,例如按桌位和時間建立可驗證的鎖或交易約束;應用層鎖只作為減少競爭的優化,不能作為唯一正確性保障。
使用者連續點擊建立怎麼辦?
要求客戶端提供冪等鍵,伺服器按餐廳和使用者維度保存結果。相同鍵返回原預約,不同鍵仍需經過統一衝突檢查,不能用使用者限流取代業務冪等。
如何優化熱門晚餐時段?
按餐廳和時間分片,快取只用於查詢提示,最終建立仍走強一致臨界區。可以預計算容量桶和候補候選,但必須在寫入時驗證版本,避免快取過期造成超賣。
預約取消後通知失敗怎麼辦?
狀態變更與 outbox 事件在同一交易中提交,通知消費者冪等重試。桌位釋放不應等待簡訊成功,通知失敗進入重試和人工可見的死信佇列。