本章探討電梯系統(Elevator System)的物件導向設計。與其他熱門題目相比,這道題更強調行為建模而非資料建模。我們的方法將聚焦於設計關鍵元件:如何表示真實世界的電梯、電梯狀態、走廊呼叫請求,以及決定電梯移動的演算法。
需求蒐集#
題目情境#
想像你在一棟辦公大樓裡,許多相同的電梯車廂可達同一組樓層。你按下「上」或「下」鍵,電梯很快就到了。進入後選擇目標樓層,電梯便載你前往。背景中,系統有效率地分派電梯並忽略反方向的請求。請設計這樣的系統。
需求釐清對話#
Candidate:是設計辦公大樓的電梯,還是包含工業電梯、貨梯?
Interviewer:僅辦公大樓。
Candidate:所有電梯都服務同一組樓層嗎?
Interviewer:是的。
Candidate:當有人按下走廊按鈕時,系統用什麼策略分派電梯?
Interviewer:策略由你決定,理想上應可設定,方便切換不同策略。可能是先進先出(FCFS)追求公平性,或其他演算法。
頂樓只應有「下」鍵、底樓只應有「上」鍵。能在設計時注意到這個細節很好,但若不是討論重點也無妨。
需求整理#
功能性需求#
- 系統管理多部電梯車廂,所有車廂服務同一組樓層
- 每個樓層有**「上」與「下」按鈕**讓使用者呼叫電梯
- 每部電梯顯示目前樓層與狀態(上行、下行、idle)
- 每部電梯有內部控制面板提供樓層按鈕
- 若使用者按下與電梯目前運行相反方向的樓層按鈕,請求應被忽略
非功能性需求#
- 分派演算法應可設定(configurable),方便切換不同最佳化策略
上述部分需求源自常識。在面試時簡短列出能確保雙方共識,並讓面試官有機會調整假設。
兩種按鈕#
電梯系統通常有兩種按鈕:
- 走廊按鈕(Hallway Buttons):位於每樓層電梯外,通常包含「上」與「下」按鈕
- 樓層按鈕(Floor Buttons):位於電梯內部控制面板,每按鈕對應一樓層
後續討論中,我們將沿用「hallway buttons」與「floor buttons」這兩個術語。
Use Case Diagram#
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。
在 OOD 中,命名比一般 coding 面試更重要。清晰的名稱能避免混淆,例如「單一電梯」與「整個電梯系統」必須區分。本設計使用 System、Dispatch、Strategy 等後綴讓概念清楚。好命名能省下解釋時間。
ElevatorCar#
ElevatorCar 模擬單一電梯的行為,維護一個目標樓層佇列追蹤待停靠的請求,並把狀態管理委派給 ElevatorStatus。
Design Choice:將電梯狀態(目前樓層、方向)獨立到
ElevatorStatus類別,提升重用性與清晰度。也支援未來擴充更多狀態屬性(如門狀態)而不需修改ElevatorCar。
ElevatorStatus#
ElevatorStatus 提供電梯目前狀態的快照,封裝 currentFloor 與 currentDirection。狀態會隨電梯移動動態更新。
替代方案:可用通用 key-value 集合儲存狀態屬性。但專屬的
ElevatorStatus提供型別安全與可擴充性——新增屬性(如維護狀態)不會影響其他元件。
Direction#
Direction enum 提供型別安全的方向表示,包含三個值:
- UP:上行
- DOWN:下行
- IDLE:靜止
使用 enum 而非任意值能減少模糊、確保一致行為。它在最佳化中扮演關鍵角色:
- 系統能避免分派反方向請求,避免不必要的回頭
- 系統能優先選擇已朝請求樓層移動的電梯,減少等待
ElevatorDispatch 與 DispatchingStrategy#
ElevatorDispatch 管理走廊與樓層按鈕的請求,決定並選擇合適的電梯。「dispatch」在此指分派電梯並指引它停靠。從電梯角度看,接送都視為路徑上的停靠點。
分派邏輯依賴 Strategy Pattern,讓系統能動態切換不同最佳化演算法。
想了解 Strategy Pattern,請參考 Parking Lot 章節。
分派流程三步驟#
- 檢視請求與電梯狀態:評估請求樓層、方向與所有電梯狀態
- 選擇最佳電梯:依
DispatchingStrategy決定,可能依:- 接近度:與請求樓層的距離
- 行進方向:已朝請求方向行進者優先
- 最短等待時間:總體等待時間最小化
- 更新下一停靠點:將請求樓層加入該電梯的待停清單
dispatchElevatorCar(int floor, Direction direction, List<ElevatorCar> elevators):處理走廊按鈕請求
DispatchingStrategy 介面#
DispatchingStrategy 定義走廊按鈕被按下時選擇電梯的具體規則。
常見分派策略#
First Come, First Serve(FCFS):依分派佇列的順序指派電梯,不論方向或接近度。簡單但繁忙時不一定最有效。
Shortest Seek Time First(SSTF):選擇能最快到達請求樓層的電梯,依兩個關鍵因素:
- 電梯為 idle 或正朝請求方向移動
- 從中選擇最接近請求樓層者,以最小化等待時間
動態策略:分派策略可依交通模式動態切換。例如尖峰時段用「高吞吐」策略、離峰時用「先進先出」。
完整類別圖#
程式實作#
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— 處理走廊呼叫,委派給ElevatorDispatchselectFloor— 處理電梯內樓層選擇,直接交由電梯本身
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 Choice:
targetFloors用Queue是為了 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;
}
}本章小結#
主要收穫:
- 模組化與職責分離:每個元件(
ElevatorSystem、ElevatorCar、ElevatorDispatch、DispatchingStrategy)各司其職- 模組化讓系統能輕鬆切換分派策略,並針對不同樓層流量最佳化
深度討論中我們探討了:
- 用 Observer Pattern 實現事件驅動的走廊呼叫處理
- 處理服務不同樓層集合的電梯
延伸閱讀:Observer Design Pattern#
Observer Design Pattern#
Observer 是一種行為型設計模式,定義訂閱機制,讓多個物件能在被觀察物件狀態改變時自動接收通知。
問題情境:新聞應用#
開發即時新聞推播應用,每當有突發新聞,所有訂閱該分類的使用者都應立即收到通知。直接連結新聞發布者與每位使用者會造成緊密耦合,難以維護。
解法#
Observer 模式建立發布者與訂閱者間的一對多關係:
- subject(publisher):新聞應用,持有業務邏輯與內容
- observers(subscribers):訂閱使用者
- 應用維護訂閱者清單,狀態改變時呼叫每位訂閱者的 update 方法
何時使用#
- 一個物件變動需通知其他物件,特別是被通知物件集合不固定或動態改變
- 物件需在特定條件或有限時間內觀察其他物件