本章探討電梯系統(Elevator System)的物件導向設計。與其他熱門題目相比,這道題更強調行為建模而非資料建模。我們的方法將聚焦於設計關鍵元件:如何表示真實世界的電梯、電梯狀態、走廊呼叫請求,以及決定電梯移動的演算法。

Elevator System

需求蒐集#

題目情境#

想像你在一棟辦公大樓裡,許多相同的電梯車廂可達同一組樓層。你按下「上」或「下」鍵,電梯很快就到了。進入後選擇目標樓層,電梯便載你前往。背景中,系統有效率地分派電梯並忽略反方向的請求。請設計這樣的系統。

需求釐清對話#

Candidate:是設計辦公大樓的電梯,還是包含工業電梯、貨梯?

Interviewer:僅辦公大樓。

Candidate:所有電梯都服務同一組樓層嗎?

Interviewer:是的。

Candidate:當有人按下走廊按鈕時,系統用什麼策略分派電梯?

Interviewer:策略由你決定,理想上應可設定,方便切換不同策略。可能是先進先出(FCFS)追求公平性,或其他演算法。

頂樓只應有「下」鍵、底樓只應有「上」鍵。能在設計時注意到這個細節很好,但若不是討論重點也無妨。

需求整理#

功能性需求#

  • 系統管理多部電梯車廂,所有車廂服務同一組樓層
  • 每個樓層有**「上」與「下」按鈕**讓使用者呼叫電梯
  • 每部電梯顯示目前樓層與狀態(上行、下行、idle)
  • 每部電梯有內部控制面板提供樓層按鈕
  • 若使用者按下與電梯目前運行相反方向的樓層按鈕,請求應被忽略

非功能性需求#

  • 分派演算法應可設定(configurable),方便切換不同最佳化策略

上述部分需求源自常識。在面試時簡短列出能確保雙方共識,並讓面試官有機會調整假設。

兩種按鈕#

電梯系統通常有兩種按鈕:

  • 走廊按鈕(Hallway Buttons):位於每樓層電梯外,通常包含「上」與「下」按鈕
  • 樓層按鈕(Floor Buttons):位於電梯內部控制面板,每按鈕對應一樓層

後續討論中,我們將沿用「hallway buttons」與「floor buttons」這兩個術語。

Use Case Diagram#

Use Case Diagram of Elevator Control System

Passenger 的 use cases#

  • Request Elevator:在某樓層按下走廊按鈕呼叫電梯
  • Select Floor:進入電梯後選擇目標樓層

System 的 use cases#

  • Assign Elevator:依可用性等因素選擇最合適的電梯
  • Move Elevator:於樓層間移動以接送乘客
  • Report Elevator Status:更新並回報電梯目前狀態(樓層與方向)

辨識核心物件#

  • ElevatorSystem:facade 類別提供主要介面,協調所有電梯狀態並把走廊呼叫委派給 ElevatorDispatch
  • ElevatorDispatch:處理走廊呼叫,依據分派策略選擇最合適的電梯
  • ElevatorCar:載送乘客的單一電梯,獨立運作並包含內部控制面板

類別圖設計#

在選擇建模策略前,先思考電梯問題是更偏向邏輯還是資料

  • 使用案例清楚:可輕易想像使用者呼叫電梯、進入、選擇樓層的過程
  • 資料模型較不明確:要建模門、按鈕、乘客嗎?

由於使用案例比資料模型更清晰,我們從系統的行為與互動著手,再以此引導類別與方法的定義。

ElevatorSystem#

ElevatorSystem 是中央控制器,提供操作所有電梯的 API。從 use case diagram 可識別三個核心職責:

  • 取得狀態:檢查電梯目前狀態(樓層與方向),用於內外顯示
  • 呼叫電梯:使用者按走廊按鈕時,系統觸發分派
  • 選擇目標樓層:使用者進入後按下控制面板的樓層按鈕

為避免 ElevatorSystem 過於複雜,我們以組合方式委派分派任務給 ElevatorDispatch

ElevatorSystem class diagram

在 OOD 中,命名比一般 coding 面試更重要。清晰的名稱能避免混淆,例如「單一電梯」與「整個電梯系統」必須區分。本設計使用 System、Dispatch、Strategy 等後綴讓概念清楚。好命名能省下解釋時間。

ElevatorCar#

ElevatorCar 模擬單一電梯的行為,維護一個目標樓層佇列追蹤待停靠的請求,並把狀態管理委派給 ElevatorStatus

ElevatorCar UML class diagram

Design Choice:將電梯狀態(目前樓層、方向)獨立到 ElevatorStatus 類別,提升重用性與清晰度。也支援未來擴充更多狀態屬性(如門狀態)而不需修改 ElevatorCar

ElevatorStatus#

ElevatorStatus 提供電梯目前狀態的快照,封裝 currentFloorcurrentDirection。狀態會隨電梯移動動態更新。

ElevatorStatus class diagram

替代方案:可用通用 key-value 集合儲存狀態屬性。但專屬的 ElevatorStatus 提供型別安全可擴充性——新增屬性(如維護狀態)不會影響其他元件。

Direction#

Direction enum 提供型別安全的方向表示,包含三個值:

  • UP:上行
  • DOWN:下行
  • IDLE:靜止

使用 enum 而非任意值能減少模糊、確保一致行為。它在最佳化中扮演關鍵角色:

  • 系統能避免分派反方向請求,避免不必要的回頭
  • 系統能優先選擇已朝請求樓層移動的電梯,減少等待

Direction enum diagram

ElevatorDispatch 與 DispatchingStrategy#

ElevatorDispatch 管理走廊與樓層按鈕的請求,決定並選擇合適的電梯。「dispatch」在此指分派電梯並指引它停靠。從電梯角度看,接送都視為路徑上的停靠點。

分派邏輯依賴 Strategy Pattern,讓系統能動態切換不同最佳化演算法。

想了解 Strategy Pattern,請參考 Parking Lot 章節。

分派流程三步驟#

  1. 檢視請求與電梯狀態:評估請求樓層、方向與所有電梯狀態
  2. 選擇最佳電梯:依 DispatchingStrategy 決定,可能依:
    • 接近度:與請求樓層的距離
    • 行進方向:已朝請求方向行進者優先
    • 最短等待時間:總體等待時間最小化
  3. 更新下一停靠點:將請求樓層加入該電梯的待停清單

ElevatorDispatch class diagram

  • dispatchElevatorCar(int floor, Direction direction, List<ElevatorCar> elevators):處理走廊按鈕請求

DispatchingStrategy 介面#

DispatchingStrategy 定義走廊按鈕被按下時選擇電梯的具體規則。

DispatchingStrategy interface diagram

常見分派策略#

First Come, First Serve(FCFS):依分派佇列的順序指派電梯,不論方向或接近度。簡單但繁忙時不一定最有效。

Shortest Seek Time First(SSTF):選擇能最快到達請求樓層的電梯,依兩個關鍵因素:

  • 電梯為 idle 或正朝請求方向移動
  • 從中選擇最接近請求樓層者,以最小化等待時間

動態策略:分派策略可依交通模式動態切換。例如尖峰時段用「高吞吐」策略、離峰時用「先進先出」。

完整類別圖#

Class Diagram of Elevator System

程式實作#

ElevatorSystem#

ElevatorSystem 透過組合管理多部 ElevatorCar,並把分派核心任務委派給 ElevatorDispatch。建構子接受兩個參數:

  • ElevatorCar 清單(系統內所有電梯)
  • DispatchingStrategy 物件(定義分派方式)

允許在建構子注入策略,讓系統能在執行期適應不同分派策略。

public class ElevatorSystem {

    private final List<ElevatorCar> elevators;
    private final ElevatorDispatch dispatchController;

    public ElevatorSystem(List<ElevatorCar> elevators, DispatchingStrategy strategy) {
        this.elevators = elevators;
        this.dispatchController = new ElevatorDispatch(strategy);
    }

    // Returns the current status of all elevators in the system
    public List<ElevatorStatus> getAllElevatorStatuses() {
        List<ElevatorStatus> statuses = new ArrayList<>();
        for (ElevatorCar elevator : elevators) {
            statuses.add(elevator.getStatus());
        }
        return statuses;
    }

    // Handles a request for an elevator from a specific floor and direction
    public void requestElevator(int currentFloor, Direction direction) {
        dispatchController.dispatchElevatorCar(currentFloor, direction, elevators);
    }

    // Handles a floor selection request from inside an elevator
    public void selectFloor(ElevatorCar car, int destinationFloor) {
        car.addFloorRequest(destinationFloor);
    }
}

主要方法:

  • getAllElevatorStatuses — 提供所有電梯的即時狀態
  • requestElevator — 處理走廊呼叫,委派給 ElevatorDispatch
  • selectFloor — 處理電梯內樓層選擇,直接交由電梯本身

ElevatorCar#

public class ElevatorCar {
    private ElevatorStatus status;
    private final Queue<Integer> targetFloors;

    public ElevatorCar(int startingFloor) {
        this.status = new ElevatorStatus(startingFloor, Direction.IDLE);
        this.targetFloors = new LinkedList<>();
    }

    // Returns the current state of the elevator
    public ElevatorStatus getStatus() {
        return status;
    }

    // Adds a new floor request if it's not already in the queue
    public void addFloorRequest(int floor) {
        if (!targetFloors.contains(floor)) {
            targetFloors.offer(floor);
            updateDirection(floor);
        }
    }

    // Checks if elevator has no pending floor requests
    public boolean isIdle() {
        return targetFloors.isEmpty();
    }

    // Updates elevator direction based on target floor position
    private void updateDirection(int targetFloor) {
        if (status.getCurrentFloor() < targetFloor) {
            status = new ElevatorStatus(status.getCurrentFloor(), Direction.UP);
        } else if (status.getCurrentFloor() > targetFloor) {
            status = new ElevatorStatus(status.getCurrentFloor(), Direction.DOWN);
        }
    }
    // getters are omitted for brevity
}

關鍵行為:

  • 抵達目標樓層時,從佇列移除該樓層;若佇列空了則進入 IDLE
  • 電梯在 targetFloors 為空時進入 idle 狀態

Implementation ChoicetargetFloorsQueue 是為了 FIFO 行為——按請求順序處理樓層,維持公平。

替代方案PriorityQueue 可依距離或方向排序以節省行進時間,但會讓乘客困惑(停靠順序不照請求順序),且需額外邏輯避免反方向停靠。Queue 略慢但更簡單、更公平、更貼近真實電梯運作。

ElevatorDispatch#

public class ElevatorDispatch {

    private final DispatchingStrategy strategy;

    public ElevatorDispatch(DispatchingStrategy strategy) {
        this.strategy = strategy;
    }

    // Handles requests from the hallway button and assigns an elevator based on the dispatching
    // strategy.
    public void dispatchElevatorCar(int floor, Direction direction, List<ElevatorCar> elevators) {
        ElevatorCar selectedElevator = strategy.selectElevator(elevators, floor, direction);
        if (selectedElevator != null) {
            selectedElevator.addFloorRequest(floor);
        }
    }
}

替代方案:可維護一個依適合度排序的 priority queue 來縮短選擇時間。但 priority queue 需隨電梯移動持續更新,對少量電梯而言開銷不划算。線性遍歷 list 雖為 O(n),但鑒於電梯數量通常不多且需策略彈性,這是可接受的選擇。

Dispatching Strategy#

First-Come-First-Serve#

選擇第一個 idle 或同方向行進的電梯;若無則隨機挑一個。

public class FirstComeFirstServeStrategy implements DispatchingStrategy {
    // Selects the first available elevator that is either idle or moving in the same direction
    @Override
    public ElevatorCar selectElevator(List<ElevatorCar> elevators, int floor, Direction direction) {
        for (ElevatorCar elevator : elevators) {
            // Return first elevator that is idle or moving in the same direction
            if (elevator.isIdle() || elevator.getCurrentDirection() == direction) {
                return elevator;
            }
        }
        // If no suitable elevator is found, randomly select one
        return elevators.get((int) (Math.random() * elevators.size()));
    }
}

Shortest-Seek-Time-First#

優先選擇最接近請求樓層的電梯:

  • 優先考慮 idle 或同方向行進者
  • 距離相同時,選擇先遇到的
public class ShortestSeekTimeFirstStrategy implements DispatchingStrategy {
    // Selects the elevator that is closest to the requested floor and moving in the same direction
    @Override
    public ElevatorCar selectElevator(List<ElevatorCar> elevators, int floor, Direction direction) {
        ElevatorCar bestElevator = null;
        int shortestDistance = Integer.MAX_VALUE;

        for (ElevatorCar elevator : elevators) {
            // Calculate distance between elevator and requested floor
            int distance = Math.abs(elevator.getCurrentFloor() - floor);
            // Select elevator if it's idle or moving in the same direction and closer than the
            // current best
            if ((elevator.isIdle() || elevator.getCurrentDirection() == direction)
                    && distance < shortestDistance) {
                bestElevator = elevator;
                shortestDistance = distance;
            }
        }

        return bestElevator;
    }
}

深度討論#

目前設計中,走廊按鈕與樓層按鈕請求都加入電梯內的同一佇列,這有兩個限制:

  • 元件耦合過緊:按鈕邏輯與分派處理互鎖,修改其一需動到另一
  • 高峰負載延遲:高流量時順序處理請求會引入延遲

事件驅動的請求處理#

引入 Observer Pattern 解耦走廊按鈕與分派控制器,讓兩者透過事件通知互動,而非依賴順序處理佇列。

運作方式#

  • Observable Subject:走廊按鈕作為 subject。按下「上」或「下」會觸發 observer 事件,自動通知分派控制器
  • Observer:分派控制器監聽按鈕事件並分派合適電梯

想了解 Observer Pattern,請參考章末**延伸閱讀**。

程式變更#

// Observable Subject: HallwayButtonPanel
public class HallwayButtonPanel {
    private final int floor;
    private final List<ElevatorObserver> observers;

    public HallwayButtonPanel(int floor) {
        this.floor = floor;
        this.observers = new ArrayList<>();
    }

    // Handles button press event and notifies all registered observers
    public void pressButton(Direction direction) {
        notifyObservers(direction);
    }

    // Registers a new observer to receive button press notifications
    public void addObserver(ElevatorObserver observer) {
        observers.add(observer);
    }

    // Notifies all registered observers about the button press
    private void notifyObservers(Direction direction) {
        for (ElevatorObserver observer : observers) {
            observer.update(floor, direction);
        }
    }
}

// Observer Interface
public interface ElevatorObserver {
    void update(int floor, Direction direction);
}

// Observer Implementation: ElevatorDispatchController
public class ElevatorDispatchController implements ElevatorObserver {
    @Override
    public void update(int floor, Direction direction) {
        // Logic to handle the floor request
    }
}

優點

  • 解耦的架構:Observer Pattern 分離走廊按鈕與分派邏輯,更易維護、測試與擴充
  • 尖峰時段更快回應:將按鈕事件視為離散事件,繞過佇列堆積,請求能即時處理

處理服務不同樓層集合的電梯#

目前所有電梯都能停靠所有樓層。若某些電梯只服務特定樓層怎麼辦?

設計需允許每部電梯持有可服務樓層集合,並確保系統只把走廊請求分派給能停該樓層的電梯。

範例情境#

20 層大樓中:

  • 電梯 1:只服務 1、5、10、15、20 樓
  • 電梯 2:服務所有樓層

當 3 樓使用者按「上」鍵時,系統避免分派電梯 1(無法停 3 樓),改派電梯 2。

設計與程式變更#

更新 ElevatorCar:新增 accessibleFloors 屬性。

// Set of floors this elevator can service
private final Set<Integer> accessibleFloors;

修改 addFloorRequest:只在樓層可達時才加入。

public void addFloorRequest(int floor) {
    // Only add the request if the floor is accessible by this elevator and not already in the
    // queue
    if (accessibleFloors.contains(floor) && !targetFloors.contains(floor)) {
        targetFloors.offer(floor);
        updateDirection(floor);
    }
}

更新分派策略:在 SSTF 中加入可達性檢查。

public class ShortestSeekTimeFirstStrategy implements DispatchingStrategy {
    @Override
    public ElevatorCar selectElevator(List<ElevatorCar> elevators, int floor, Direction direction) {
        ElevatorCar bestElevator = null;
        int shortestDistance = Integer.MAX_VALUE;

        for (ElevatorCar elevator : elevators) {
            // Calculate distance between elevator and requested floor
            int distance = Math.abs(elevator.getCurrentFloor() - floor);
            // Select elevator if it's idle or moving in the same direction and closer than the
            // current best
            if ((elevator.isIdle() || elevator.getCurrentDirection() == direction)
                    // Only consider elevators that can actually reach the requested floor
                    && elevator.getAccessibleFloors().contains(floor)
                    && distance < shortestDistance) {
                bestElevator = elevator;
                shortestDistance = distance;
            }
        }

        return bestElevator;
    }
}

本章小結#

主要收穫

  • 模組化與職責分離:每個元件(ElevatorSystemElevatorCarElevatorDispatchDispatchingStrategy)各司其職
  • 模組化讓系統能輕鬆切換分派策略,並針對不同樓層流量最佳化

深度討論中我們探討了:

  • Observer Pattern 實現事件驅動的走廊呼叫處理
  • 處理服務不同樓層集合的電梯

延伸閱讀:Observer Design Pattern#

Observer Design Pattern#

Observer 是一種行為型設計模式,定義訂閱機制,讓多個物件能在被觀察物件狀態改變時自動接收通知。

問題情境:新聞應用#

開發即時新聞推播應用,每當有突發新聞,所有訂閱該分類的使用者都應立即收到通知。直接連結新聞發布者與每位使用者會造成緊密耦合,難以維護。

解法#

Observer 模式建立發布者與訂閱者間的一對多關係

  • subject(publisher):新聞應用,持有業務邏輯與內容
  • observers(subscribers):訂閱使用者
  • 應用維護訂閱者清單,狀態改變時呼叫每位訂閱者的 update 方法

Observer design pattern class diagram

何時使用#

  • 一個物件變動需通知其他物件,特別是被通知物件集合不固定或動態改變
  • 物件需在特定條件或有限時間內觀察其他物件