本章探討**餐廳管理系統(Restaurant Management System)**的設計。目標是建立代表選單、訂位、桌次等元件的類別。系統需支援預約訂位、管理訂單、分配桌次,並保持設計簡潔且易於擴展。

Restaurant reservation

需求蒐集#

題目情境#

想像你規劃週五晚的餐聚。你致電餐廳訂位、查可用時段、確認位置。到達時店員依預約安排桌次、接受點餐、最後出帳。背景中,系統處理訂位、追蹤訂單、計算費用。請設計這樣的系統。

需求釐清對話#

Candidate:先設定範圍。系統處理訂位、選單、訂單追蹤、付款。我先聚焦訂位與訂單管理可以嗎?

Interviewer:合理的起點。

Candidate:顧客可建立與管理訂位嗎?

Interviewer:可以,依可用性訂未來日期與時間。

Candidate:如何判斷桌次可用?

Interviewer:找一張容納人數且該時段未被預訂的桌。每筆訂位佔用桌一小時。

Candidate:可取消訂位嗎?

Interviewer:可以。

Candidate:有預約者抵達時自動取得預約桌?

Interviewer:是。以姓名查詢已分配的桌。

Candidate:未預約的 walk-in 怎麼處理?

Interviewer:依現有可用性與人數分派桌次。

Candidate:訂單下單後可變更或移除嗎?

Interviewer:可以,能移除品項或調整數量。

Candidate:系統追蹤訂單狀態嗎?

Interviewer:是的。

Candidate:結帳要拆帳嗎?

Interviewer:先呈現單一總金額即可。

需求整理#

訂位#

  • 顧客可依可用性預訂未來日期/時間
  • 每筆訂位佔用一小時
  • 系統檢查目標時段桌次是否無重疊預訂
  • 系統為訂位指派桌次;顧客以姓名認領
  • 顧客可取消訂位

Walk-in 入座#

  • 依當下可用性與人數指派 walk-in 桌次

訂單管理#

  • 下單後可變更或移除
  • 系統追蹤訂單進度

結帳#

  • 結帳呈現單一總金額

非功能性需求#

  • 在尖峰時段(如週末晚上)能流暢處理高流量

辨識核心物件#

  • Menu:儲存可點品項集合
  • MenuItem:單一品項,封裝名稱與價格
  • Layout:餐廳實體配置,組織所有桌次以快速分派
  • Table:單一桌次,含容量、目前訂位、進行中訂單
  • Reservation:訂位細節(姓名、人數、時間、指派桌)
  • ReservationManager:管理所有訂位,檢查可用性並與 Layout 協作分派桌次
  • Restaurant:作為 facade 提供管理訂位、桌次、訂單、結帳的中央介面

類別圖設計#

Menu 以名稱為 key 將品項存於 map 中,支援快速查找。

Menu class diagram

MenuItem 定義單一品項,含名稱、描述、價格、分類。Category enum 賦予類型(main course、appetizer、dessert)。

MenuItem class and Category enum

Table#

Table 模擬餐廳桌次。部分屬性少變動(capacitytableId),其他則代表會隨時間變動的當前狀態(reservationsorderedItems)。

Table class diagram

它管理桌次目前使用狀況,與 Layout 協作做可用性檢查、與 OrderItem 處理訂單。

Layout#

Layout 監督所有桌次,依 ID 與容量組織以便為每筆訂位找到合適的桌。

Design Choice:將桌次組織獨立於 Layout 是為了最佳化分派效率,並與選單/訂單邏輯分離。若把桌次分派整合進 Restaurant,雖能簡化結構,但會讓 facade 同時處理高層協調與低層管理。

Layout class diagram

OrderItem#

OrderItem 代表顧客點的單一品項,連結到 MenuItem。它追蹤品項狀態,讓 Table 能計算費用與管理品項。Status enum(pending、delivered…)表示目前狀態。

OrderItem class and Status enum

ReservationManager#

ReservationManager 處理訂位排程——尋找可用時段、建立訂位、處理取消。它儲存 Reservation 物件並透過 Layout 分配桌次。

ReservationManager class diagram

Reservation#

Reservation 儲存單筆預訂細節:姓名、人數、時間、指派桌次。

Reservation class diagram

Restaurant#

Restaurant 作為系統的主要介面與 facade,協調訂位、桌次分派、訂單、結帳:

  • ReservationManager:排程與取消訂位
  • Layout(透過 ReservationManager):辨識可用桌次
  • Menu:提供訂單品項
  • Table:訂單與帳單處理

Design Choice:將 Restaurant 設計為 facade 統一系統操作。若把所有邏輯內建於 Restaurant,會增加複雜度並降低可擴展性。

Restaurant class diagram

完整類別圖#

Class Diagram of Restaurant Management System

程式實作#

Implementation Choice:用 HashMap 提供以 key 為基礎的快速查找;List 則需線性搜尋。

public class Menu {
    private final Map<String, MenuItem> menuItems = new HashMap<>();

    // Adds a new item to the menu
    public void addItem(MenuItem item) {
        menuItems.put(item.getName(), item);
    }

    public MenuItem getItem(String name) {
        return menuItems.get(name);
    }

    public Map<String, MenuItem> getMenuItems() {
        return Collections.unmodifiableMap(menuItems);
    }
}

MenuItemprivate final 欄位確保不可變性。價格用 BigDecimal 提供精確控制,避免浮點捨入錯誤。

// Represents a single item available on the restaurant menu
public class MenuItem {
    private final String name;
    private final String description;
    private final BigDecimal price;
    private final Category category;

    public MenuItem(String name, String description, BigDecimal price, Category category) {
        this.name = name;
        this.description = description;
        this.price = price;
        this.category = category;
    }

    // Enumeration of possible menu item categories
    public enum Category {
        MAIN,
        APPETIZER,
        DESSERT
    } // getter methods are omitted for brevity
}

Table#

// Represents a table in the restaurant with its properties and current state
public class Table {
    // immutable properties
    private final int tableId;
    private final int capacity;

    // current state
    private final Map<LocalDateTime, Reservation> reservations = new HashMap<>();
    private final Map<MenuItem, List<OrderItem>> orderedItems = new HashMap<>();

    public Table(int tableId, int capacity) {
        this.tableId = tableId;
        this.capacity = capacity;
    }

    // Calculates the total bill amount for all ordered items at this table
    public BigDecimal calculateBillAmount() {
        return orderedItems.values().stream()
                .flatMap(List::stream)
                .map(OrderItem::getItem)
                .map(MenuItem::getPrice)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    // Adds multiple orders of the same menu item to the table
    public void addOrder(MenuItem item, int quantity) {
        for (int i = 0; i < quantity; i++) {
            addOrder(item);
        }
    }

    // Adds a single menu item to the table's order
    public void addOrder(MenuItem item) {
        List<OrderItem> orderItems = orderedItems.get(item);
        if (orderItems == null) {
            orderItems = new ArrayList<>();
            orderedItems.put(item, orderItems);
            orderItems.add(new OrderItem(item));
        } else {
            orderItems.add(new OrderItem(item));
        }
    }

    // Removes a menu item from the table's order
    public void removeOrder(MenuItem item) {
        List<OrderItem> orderItems = orderedItems.get(item);
        if (orderItems != null) {
            orderItems.remove(0);
            if (orderItems.isEmpty()) {
                orderedItems.remove(item);
            }
        }
    }

    // Checks if the table is available at a specific time
    public boolean isAvailableAt(LocalDateTime reservationTime) {
        return !reservations.containsKey(reservationTime);
    }

    // Adds a reservation to this table
    public void addReservation(Reservation reservation) {
        reservations.put(reservation.getTime(), reservation);
    }

    // Removes a reservation from this table for a specific time
    public void removeReservation(LocalDateTime reservationTime) {
        reservations.remove(reservationTime);
    }

    // getter methods are omitted for brevity
}

關鍵結構:

  • Map(reservation time → Reservation):快速可用性檢查
  • Map(MenuItem → OrderItem list):管理訂單,支援 addOrderremoveOrdercalculateBillAmount

Layout#

Layout 用兩種索引方式管理所有桌次:

  • 依 ID(tablesById):以唯一 ID 快速取得
  • 依容量(tablesByCapacity):用 SortedMap 將容量映射到桌集合,讓 findAvailableTable 能快速找到最小可用桌
// Manages the collection of tables in the restaurant and their arrangement
public class Layout {
    private final Map<Integer, Table> tablesById = new HashMap<>();
    // Groups tables by their capacity for efficient table assignment, sorted from smallest to
    // largest capacity
    private final SortedMap<Integer, Set<Table>> tablesByCapacity = new TreeMap<>();

    public Layout(List<Integer> tableCapacities) {
        for (int i = 0; i < tableCapacities.size(); i++) {
            int capacity = tableCapacities.get(i);
            Table table = new Table(i, capacity);
            tablesById.put(i, table);
            tablesByCapacity.computeIfAbsent(capacity, k -> new HashSet<>()).add(table);
        }
    }

    // Finds the smallest available table that can accommodate a party of the given size at the
    // given time
    public Table findAvailableTable(int partySize, LocalDateTime reservationTime) {
        for (Set<Table> tables : tablesByCapacity.tailMap(partySize).values()) {
            for (Table table : tables) {
                if (table.isAvailableAt(reservationTime)) {
                    return table;
                }
            }
        }
        return null;
    }
}

Implementation ChoiceSortedMap 提供有序的 key 存取,便於依容量做範圍搜尋——對 findAvailableTable 配對人數至關重要。基本 Map 則需額外邏輯找最小合適桌。

OrderItem#

狀態轉移方法:

  • sendToKitchenPENDINGSENT_TO_KITCHEN
  • deliverToCustomerSENT_TO_KITCHENDELIVERED
  • cancel:未送達時設為 CANCELED
// Represents a food item ordered by a customer with its current status in the order process
public class OrderItem {
    private final MenuItem item;
    private Status status = Status.PENDING;

    public OrderItem(MenuItem item) {
        this.item = item;
    }

    // Updates the status to indicate the item has been sent to the kitchen
    public void sendToKitchen() {
        if (status == Status.PENDING) status = Status.SENT_TO_KITCHEN;
    }

    // Updates the status to indicate the item has been delivered to the customer
    public void deliverToCustomer() {
        if (status == Status.SENT_TO_KITCHEN) status = Status.DELIVERED;
    }

    // Updates the status to indicate the item has been canceled
    public void cancel() {
        if (status == Status.PENDING || status == Status.SENT_TO_KITCHEN) {
            status = Status.CANCELED;
        }
    }

    // getter methods are omitted for brevity
}

ReservationManager#

// Manages all reservations for the restaurant and handles table assignments
public class ReservationManager {
    private final Layout layout;
    private final Set<Reservation> reservations = new HashSet<>();

    // Constructor that takes the restaurant's table layout
    public ReservationManager(Layout layout) {
        this.layout = layout;
    }

    // Finds potential time slots for a reservation within the given time range and party size
    public LocalDateTime[] findAvailableTimeSlots(
            LocalDateTime rangeStart, LocalDateTime rangeEnd, int partySize) {
        // checking every hour in the time range
        LocalDateTime current = rangeStart;
        List<LocalDateTime> possibleReservations = new ArrayList<>();
        while (!current.isAfter(rangeEnd)) {
            Table availableTable = layout.findAvailableTable(partySize, current);
            if (availableTable != null) {
                possibleReservations.add(current);
            }
            current = current.plusHours(1);
        }
        return possibleReservations.toArray(new LocalDateTime[0]);
    }

    // Creates a reservation for a specific time, party size and name
    public Reservation createReservation(
            String partyName, int partySize, LocalDateTime desiredTime) {
        desiredTime = desiredTime.truncatedTo(ChronoUnit.HOURS);
        Table table = layout.findAvailableTable(partySize, desiredTime);
        Reservation reservation = new Reservation(partyName, partySize, desiredTime, table);
        table.addReservation(reservation);
        reservations.add(reservation);
        return reservation;
    }

    // Removes an existing reservation
    public void removeReservation(
            String partyName, int partySize, LocalDateTime reservationTime) {
        // Find matching reservation before removing it
        for (Reservation reservation : new HashSet<>(reservations)) {
            if (reservation.getTime().equals(reservationTime)
                    && reservation.getPartySize() == partySize
                    && reservation.getPartyName().equals(partyName)) {
                // Clear the reservation from the table first
                Table table = reservation.getAssignedTable();
                table.removeReservation(reservationTime);
                // Then remove from the reservation collection
                reservations.remove(reservation);
                return;
            }
        }
    }
    // getter methods are omitted for brevity
}

Implementation Choice:用 Set 儲存 Reservation 防止重複預訂。List 需額外檢查;以時間為 key 的 Map 雖加快查找但需複合 key 才能正確處理移除。

Reservation#

// Represents a reservation made at the restaurant for a specific party, time and table
public class Reservation {
    private final String partyName;
    private final int partySize;
    private final LocalDateTime time;
    private final Table assignedTable;

    public Reservation(
            String partyName, int partySize, LocalDateTime time, Table assignedTable) {
        this.partyName = partyName;
        this.partySize = partySize;
        this.time = time;
        this.assignedTable = assignedTable;
    }

    // getter methods are omitted for brevity
}

Restaurant#

// Main restaurant class that manages reservations, orders, and tables
public class Restaurant {
    private final String name;
    private final Menu menu;
    private final Layout layout;
    private final ReservationManager reservationManager;

    public Restaurant(String name, Menu menu, Layout layout) {
        this.name = name;
        this.menu = menu;
        this.layout = layout;
        this.reservationManager = new ReservationManager(layout);
    }

    // Finds possible reservation times within a time range for a party of specified size
    public LocalDateTime[] findAvailableTimeSlots(
            LocalDateTime rangeStart, LocalDateTime rangeEnd, int partySize) {
        return reservationManager.findAvailableTimeSlots(rangeStart, rangeEnd, partySize);
    }

    // Creates a reservation for a party at the specified time
    public Reservation createScheduledReservation(
            String partyName, int partySize, LocalDateTime time) {
        return reservationManager.createReservation(partyName, partySize, time);
    }

    // Removes an existing reservation
    public void removeReservation(
            String partyName, int partySize, LocalDateTime reservationTime) {
        reservationManager.removeReservation(partyName, partySize, reservationTime);
    }

    // Creates a reservation for a party without prior reservation
    public Reservation createWalkInReservation(String partyName, int partySize) {
        return reservationManager.createReservation(partyName, partySize, LocalDateTime.now());
    }

    // Adds an item to a table's order
    public void orderItem(Table table, MenuItem item) {
        table.addOrder(item);
    }

    // Removes an item from a table's order
    public void cancelItem(Table table, MenuItem item) {
        table.removeOrder(item);
    }

    // Calculates the bill amount for a table
    public BigDecimal calculateTableBill(Table table) {
        return table.calculateBillAmount();
    }

    // getter methods are omitted for brevity
}

關鍵特性:

  • 訂位方法findAvailableTimeSlotscreateScheduledReservationremoveReservation,皆委派給 ReservationManager
  • Walk-in 入座createWalkInReservation
  • 訂單管理orderItemcancelItem 委派給 Table
  • 結帳calculateTableBill 委派給 Table.calculateBillAmount

Implementation ChoiceRestaurant 設為 facade 不持有自己的資料結構,所有操作皆委派。內建訂位或訂單會增加複雜度並降低模組化。

深度討論#

訂單佇列追蹤#

在高流量情境下(如週五晚),現有系統由 Table 直接管理 OrderItem 狀態。這種去中心化結構缺乏跨桌訂單進度的整體視圖,店員難以:

  • 優先處理時間敏感的訂單
  • 監控廚房延遲
  • 不逐桌檢查就能驗證取消

我們引入集中式的 OrderManager 類別來佇列與處理訂單動作。

Step 1:定義 Command 介面#

public interface OrderCommand {
    void execute();
}

Step 2:實作具體 Command#

每個任務有自己的類別,與原系統 Table 直接更新 OrderItem 狀態不同:

  • SendToKitchenCommand:呼叫 sendToKitchen()
  • DeliverCommand:呼叫 deliverToCustomer()
  • CancelCommand:呼叫 cancel()
// Command that handles sending order items to the Kitchen
public class SendToKitchenCommand implements OrderCommand {
    private final OrderItem orderItem;

    public SendToKitchenCommand(OrderItem orderItem) {
        this.orderItem = orderItem;
    }

    @Override
    public void execute() {
        orderItem.sendToKitchen();
    }
}

// Command that handles delivery of order items
public class DeliverCommand implements OrderCommand {
    private final OrderItem orderItem;

    public DeliverCommand(OrderItem orderItem) {
        this.orderItem = orderItem;
    }

    @Override
    public void execute() {
        orderItem.deliverToCustomer();
    }
}

// Command that handles cancellations of order items
public class CancelCommand implements OrderCommand {
    private final OrderItem orderItem;

    public CancelCommand(OrderItem orderItem) {
        this.orderItem = orderItem;
    }

    @Override
    public void execute() {
        orderItem.cancel();
    }
}

Step 3:引入 OrderManager#

public class OrderManager {
    private final List<OrderCommand> commandQueue = new ArrayList<>();

    // Adds a command to the queue for later execution
    public void addCommand(OrderCommand command) {
        commandQueue.add(command);
    }

    // Executes all commands in the queue and clears it
    public void executeCommands() {
        for (OrderCommand command : commandQueue) {
            command.execute();
        }
        commandQueue.clear();
    }
}

Step 4:整合到 Restaurant#

public class Restaurant {
    // ... fields unchanged ...
    private final OrderManager orderManager;

    public Restaurant(String name, Menu menu, Layout layout) {
        // ... fields unchanged ...
        this.orderManager = new OrderManager();
    }

    // Adds an item to a table's order and sends it to the kitchen
    public void orderItem(Table table, MenuItem item) {
        table.addOrder(item);
        // Get the last added order item
        List<OrderItem> orderItems = table.getOrderedItems().get(item);
        if (orderItems != null && !orderItems.isEmpty()) {
            OrderItem lastOrder = orderItems.get(orderItems.size() - 1);
            OrderCommand sendToKitchen = new SendToKitchenCommand(lastOrder);
            orderManager.addCommand(sendToKitchen);
            orderManager.executeCommands();
        }
    }

    // Removes an item from a table's order and cancels it
    public void cancelItem(Table table, MenuItem item) {
        List<OrderItem> orderItems = table.getOrderedItems().get(item);
        if (orderItems != null && !orderItems.isEmpty()) {
            OrderItem lastOrder = orderItems.get(orderItems.size() - 1);
            OrderCommand cancelOrder = new CancelCommand(lastOrder);
            orderManager.addCommand(cancelOrder);
            orderManager.executeCommands();
            table.removeOrder(item);
        }
    }

    // Delivers an item to the customer
    public void deliverItem(Table table, MenuItem item) {
        List<OrderItem> orderItems = table.getOrderedItems().get(item);
        if (orderItems != null && !orderItems.isEmpty()) {
            OrderItem lastOrder = orderItems.get(orderItems.size() - 1);
            OrderCommand deliverOrder = new DeliverCommand(lastOrder);
            orderManager.addCommand(deliverOrder);
            orderManager.executeCommands();
        }
    }
    // ... other methods unchanged ...
}

Command Pattern#

我們所實作的正是 Command Pattern——一種行為型設計模式,將請求封裝為獨立物件,包含執行所需的所有細節。這讓你能將請求作為方法參數傳遞,並能延遲或排程其執行。

組成元件:

  • CommandOrderCommand 介面與其實作把每個動作定義為物件,透過 execute() 執行
  • InvokerOrderManager 佇列 command 物件並觸發執行,將 Table 與直接狀態變更解耦
  • ReceiverOrderItem 接收並處理 command,更新自身狀態

Command pattern

本章小結#

主要收穫模組化單一職責原則的重要性。每個元件(MenuReservationManagerLayoutTable)各司其職,使系統易維護、易擴展。

設計選擇如以 Restaurant 作為 facade 委派操作、使用不可變的 MenuItem,皆強調彈性與一致性。替代方案(將訂位與訂單邏輯直接放入 Restaurant)雖簡化結構,但會增加複雜度並降低可擴展性。在面試中能反思這些決策並闡述理由,正是展現系統設計批判思維的好機會。