從單體到分佈式:Java 電梯調度系統的演進與實踐

在現代建築中,電梯系統是不可或缺的垂直交通樞紐,其調度算法的優劣直接影響着用户體驗與運行效率。本文將圍繞“單部電梯調度”這一經典問題,通過三次遞進的題集,詳細闡述一個電梯調度系統從基礎功能實現到多電梯協同調度的完整演進過程。我們將深入剖析設計決策、重構優化以及在實際開發中遇到的種種“坑”,旨在展現一個真實的軟件迭代過程。

一、前言:題集概覽與核心目標

本階段三次題集圍繞“單部電梯調度系統”展開,是一套從“功能實現”到“設計優化”再到“健壯性提升”的梯度化訓練。三次題集的核心目標均為模擬真實電梯的運行邏輯,但難度與要求逐步深化:

  • 題集1:聚焦基礎面向對象、輸入解析與簡單調度邏輯,包含1道主題題和3道驗證用例,難度為入門級別,核心目標是實現電梯基礎功能(內外請求處理、移動、停靠)。
  • 題集2:重點考察單一職責原則(SRP)、類的解耦與請求去重,包含1道主題題和5道驗證用例,難度為進階級別,核心目標是優化類設計,拆分耦合邏輯並引入Passenger類。
  • 題集3:着力於調度算法優化、健壯性處理與測試驅動開發,包含1道主題題和8道驗證用例,難度為提升級別,核心目標是優化同方向優先調度邏輯,強化輸入校驗與異常處理。

這三次題集覆蓋了面向對象編程 (OOP)設計原則貪心算法正則表達式異常處理等核心知識點。難度呈階梯式上升,引導開發者從“能跑通”的初級階段,邁向“跑得好、易維護、可擴展”的工程化階段。

二、設計與分析:單部電梯調度系統的三次迭代

2.1 分析框架

我們將從以下四個維度對每次迭代的設計進行分析:

  • 類設計合理性:是否遵循單一職責原則,類的職責劃分是否清晰。
  • 代碼複雜度:方法的邏輯分支、代碼行數等。
  • 耦合度:類與類之間的依賴關係是否緊密。
  • 功能完整性:是否滿足題目的所有功能點。

2.2 題集1:基礎功能實現(耦合式設計)

2.2.1 類設計結構

在第一階段,核心目標是快速實現功能,因此設計較為簡單直接,主要包含一個全能的 Lift 類和兩個枚舉類。

  • Dir 枚舉:定義 UPDOWNSTOPPED 三個方向。
  • Stat 枚舉:定義 STOPPEDMOVINGDOOR_OPENDOOR_CLOSED 四種狀態。
  • Lift:這是一個典型的“上帝類”(God Class),集成了所有核心邏輯。
  • 屬性minFloor, maxFloor, currentFloor, direction, status,以及用於存儲請求的 List
  • 方法addInternalRequest(), addExternalRequest(), run(), shouldStop(), handleStop() 等。

這種設計的優點是直觀、易於實現,但缺點也同樣明顯:耦合度極高Lift 類負責了太多的事情,後續維護和擴展將變得非常困難。

2.2.2 核心代碼實現

以下是第一版 Lift 類的核心方法 run() 的簡化實現,它體現了電梯的基本運行邏輯:

// 偽代碼示意
public void run() {
    while (hasPendingRequests()) {
        // 確定下一個目標樓層
        Integer nextFloor = determineNextDestination();
        if (nextFloor == null) break;

        // 移動到目標樓層
        while (currentFloor != nextFloor) {
            move(); // 更新 currentFloor 並打印移動日誌
            // 檢查途中是否需要停靠
            if (shouldStopAtCurrentFloor()) {
                handleStop(); // 開門 -> 處理請求 -> 關門
            }
        }

        // 到達最終目標樓層後停靠
        if (shouldStopAtCurrentFloor()) {
            handleStop();
        }
    }
    System.out.println("所有請求處理完畢。");
}

private void move() {
    status = Stat.MOVING;
    currentFloor += (direction == Dir.UP) ? 1 : -1;
    System.out.println("Current Floor: " + currentFloor + " Direction: " + direction);
}

private void handleStop() {
    status = Stat.DOOR_OPEN;
    System.out.println("Open Door # Floor " + currentFloor);

    // 移除當前樓層的所有請求
    internalRequests.remove(currentFloor);
    externalUpRequests.remove(currentFloor);
    externalDownRequests.remove(currentFloor);

    try {
        Thread.sleep(500); // 模擬開關門時間
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }

    status = Stat.DOOR_CLOSED;
    System.out.println("Close Door");
}

2.2.3 核心分析

  • 優勢:快速實現了電梯上下移動、響應請求、開關門等基礎功能,滿足了題目的基本要求。
  • 不足
  1. 違反單一職責原則Lift 類身兼數職,負責請求管理、狀態機控制、業務邏輯(如 run 方法),導致代碼結構混亂。
  2. 請求描述不精準:外部請求僅用“樓層+方向”表示,無法完整描述乘客的意圖(從哪到哪),這為後續的調度優化帶來了困難。
  3. 去重邏輯簡陋:依賴 Listcontains 方法進行去重,效率低下且邏輯分散。

2.3 題集2:設計優化(引入 Passenger 類,解耦設計)

2.3.1 類設計結構

針對第一版的問題,我們進行了重構,核心思想是遵循單一職責原則,將不同的職責分離到不同的類中。

  • Passenger:作為數據載體,封裝乘客的源樓層(sourceFloor)和目的樓層(destFloor)。這使得請求的描述更加精準。
  • RequestQueue:專門負責請求的管理。內部維護三個 Set(保證去重):internalRequests(內部按扭)、externalUpRequests(外部上行)、externalDownRequests(外部下行)。提供 addremovecontains 等方法。
  • Lift:專注於電梯自身的狀態管理和運行邏輯。它不再直接管理 List,而是持有一個 RequestQueue 的引用,通過調用其方法來操作請求。
  • LiftController:負責解析用户輸入,並將解析後的請求(Passenger 對象或樓層號)添加到 RequestQueue 中。它是系統的“入口”。
  • DirStat 枚舉:保持不變,作為類型規範。

2.3.2 核心代碼實現

Passenger.java

public class Passenger {
    private final int sourceFloor;
    private final int destFloor;

    public Passenger(int sourceFloor, int destFloor) {
        this.sourceFloor = sourceFloor;
        this.destFloor = destFloor;
    }

    public int getSourceFloor() { return sourceFloor; }
    public int getDestFloor() { return destFloor; }
    
    // 用於判斷兩個乘客的外部請求是否相同(在同一樓層按同一方向的按鈕)
    public boolean isSameExternalRequest(Passenger other) {
        if (other == null) return false;
        return this.sourceFloor == other.sourceFloor && this.getDirection() == other.getDirection();
    }

    public Dir getDirection() {
        if (sourceFloor < destFloor) return Dir.UP;
        if (sourceFloor > destFloor) return Dir.DOWN;
        return Dir.STOPPED; // 理論上不會發生
    }

    // 重寫 equals 和 hashCode,以便在 Set 中正確去重
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Passenger passenger = (Passenger) o;
        return sourceFloor == passenger.sourceFloor && destFloor == passenger.destFloor;
    }

    @Override
    public int hashCode() {
        return Objects.hash(sourceFloor, destFloor);
    }
}

RequestQueue.java

public class RequestQueue {
    private final Set<Integer> internalRequests = new LinkedHashSet<>();
    private final Set<Passenger> externalUpRequests = new LinkedHashSet<>();
    private final Set<Passenger> externalDownRequests = new LinkedHashSet<>();

    // ... 省略 add, remove, isEmpty 等方法 ...
    
    public boolean shouldStop(int currentFloor, Dir direction) {
        // 內部請求
        if (internalRequests.contains(currentFloor)) {
            return true;
        }
        // 外部請求,需考慮方向
        if (direction == Dir.UP && externalUpRequests.stream().anyMatch(p -> p.getSourceFloor() == currentFloor)) {
            return true;
        }
        if (direction == Dir.DOWN && externalDownRequests.stream().anyMatch(p -> p.getSourceFloor() == currentFloor)) {
            return true;
        }
        return false;
    }

    public void handleStop(int currentFloor) {
        internalRequests.remove(currentFloor);
        externalUpRequests.removeIf(p -> p.getSourceFloor() == currentFloor);
        externalDownRequests.removeIf(p -> p.getSourceFloor() == currentFloor);
    }
}

Lift.java (run 方法邏輯)

public class Lift {
    private final RequestQueue requestQueue;
    // ... 其他屬性 ...

    public Lift(int minFloor, int maxFloor, RequestQueue requestQueue) {
        // ... 初始化 ...
        this.requestQueue = requestQueue;
    }

    public void run() {
        System.out.println("\n電梯開始運行...");
        while (!requestQueue.isEmpty()) {
            // 1. 確定下一個目標樓層
            Integer nextDest = determineNextDestination();
            if (nextDest == null) {
                break; // 無請求,退出循環
            }

            // 2. 移動到目標樓層
            moveTo(nextDest);

            // 3. 到達後處理停靠
            if (requestQueue.shouldStop(currentFloor, direction)) {
                handleStop();
            }
        }
        status = Stat.STOPPED;
        direction = Dir.STOPPED;
        System.out.println("所有請求處理完畢,電梯停在 " + currentFloor + " 樓。");
    }

    private void moveTo(int targetFloor) {
        direction = (currentFloor < targetFloor) ? Dir.UP : Dir.DOWN;
        while (currentFloor != targetFloor) {
            moveOneFloor(); // 封裝單次移動邏輯
            // 檢查途中是否需要停靠
            if (requestQueue.shouldStop(currentFloor, direction)) {
                handleStop();
            }
        }
    }
}

2.3.3 核心分析

  • 關鍵改進
  1. 引入 Passenger:使請求模型化,更貼近現實,為精準調度提供了可能。
  2. 引入 RequestQueue:將請求的管理邏輯(增、刪、查、去重)完全封裝起來,Lift 類通過調用其方法與請求交互,實現瞭解耦。
  3. 引入 LiftController:負責輸入解析,隔離了用户界面與業務邏輯。
  • 優勢:代碼結構清晰,職責分明。修改請求的存儲方式(如從 Set 改為 PriorityQueue)只需改動 RequestQueue,而無需修改 Lift 類。代碼的可維護性和可讀性得到了極大提升。

2.4 題集3:健壯性與算法優化(調度優化+異常處理)

2.4.1 類設計結構

類的結構與題集 2 基本保持一致,本階段的優化主要集中在算法邏輯代碼健壯性上。

  • Lift:重點優化了 determineNextDestination() 方法,實現了更智能的“同方向優先”調度策略。
  • LiftController:增強了輸入校驗邏輯,使用正則表達式嚴格匹配輸入格式,並能優雅地處理各種異常輸入(如非數字、無效樓層、錯誤方向等)。

2.4.2 核心代碼實現

Lift.java (調度算法優化)

// 在 Lift 類中
private Integer determineNextDestination() {
    // 1. 如果當前有方向(非 STOPPED),優先處理同方向的請求
    if (direction != Dir.STOPPED) {
        Integer sameDirDest = findDestinationInCurrentDirection();
        if (sameDirDest != null) {
            return sameDirDest;
        }
        // 2. 同方向無請求,切換方向
        direction = (direction == Dir.UP) ? Dir.DOWN : Dir.UP;
    }

    // 3. 如果是靜止狀態,或切換方向後,尋找最近的請求
    if (direction == Dir.STOPPED) {
        // 首次啓動或已停止,需要確定初始方向
        return findClosestRequestAndSetDirection();
    } else {
        // 切換方向後,查找該方向的目標
        Integer newDirDest = findDestinationInCurrentDirection();
        if (newDirDest != null) {
            return newDirDest;
        }
    }
    
    // 4. 無任何請求
    return null;
}

/**
 * 在當前運行方向上尋找下一個目標樓層。
 * UP 方向找大於當前樓層的最小值,DOWN 方向找小於當前樓層的最大值。
 */
private Integer findDestinationInCurrentDirection() {
    List<Integer> relevantFloors = new ArrayList<>();

    // 內部請求
    relevantFloors.addAll(requestQueue.getInternalRequests().stream()
            .filter(floor -> (direction == Dir.UP && floor > currentFloor) ||
                             (direction == Dir.DOWN && floor < currentFloor))
            .collect(Collectors.toList()));

    // 外部請求的源樓層
    relevantFloors.addAll(requestQueue.getExternalUpRequests().stream()
            .filter(p -> direction == Dir.UP && p.getSourceFloor() > currentFloor)
            .map(Passenger::getSourceFloor)
            .collect(Collectors.toList()));
    
    relevantFloors.addAll(requestQueue.getExternalDownRequests().stream()
            .filter(p -> direction == Dir.DOWN && p.getSourceFloor() < currentFloor)
            .map(Passenger::getSourceFloor)
            .collect(Collectors.toList()));

    if (relevantFloors.isEmpty()) {
        return null;
    }

    if (direction == Dir.UP) {
        return Collections.min(relevantFloors);
    } else { // DOWN
        return Collections.max(relevantFloors);
    }
}

LiftController.java (輸入處理與健壯性)

public class LiftController {
    private final Lift lift;
    private final RequestQueue requestQueue;
    private static final Pattern INTERNAL_PATTERN = Pattern.compile("^\\s*<(\\d+)>\\s*$");
    private static final Pattern EXTERNAL_PATTERN = Pattern.compile("^\\s*<(\\d+),(UP|DOWN)>\\s*$");

    public LiftController(Lift lift, RequestQueue requestQueue) {
        this.lift = lift;
        this.requestQueue = requestQueue;
    }

    public void parseAndDispatch(String input) {
        input = input.trim();
        Matcher matcher;

        if ((matcher = INTERNAL_PATTERN.matcher(input)).matches()) {
            try {
                int floor = Integer.parseInt(matcher.group(1));
                if (isFloorValid(floor)) {
                    requestQueue.addInternalRequest(floor);
                    System.out.println("已添加內部請求:" + floor + "樓");
                } else {
                    System.out.println("無效內部請求:樓層 " + floor + " 超出範圍。");
                }
            } catch (NumberFormatException e) {
                System.out.println("無效內部請求格式:" + input);
            }
        } else if ((matcher = EXTERNAL_PATTERN.matcher(input)).matches()) {
            try {
                int floor = Integer.parseInt(matcher.group(1));
                Dir dir = Dir.valueOf(matcher.group(2));
                if (isFloorValid(floor)) {
                    Passenger p = new Passenger(floor, -1); // 目的樓層未知,僅作為外部請求標識
                    boolean added = (dir == Dir.UP) ? requestQueue.addExternalUpRequest(p) : requestQueue.addExternalDownRequest(p);
                    if (added) {
                        System.out.println("已添加外部請求:" + floor + "樓 " + dir);
                    } else {
                        System.out.println("外部請求 " + input + " 已存在。");
                    }
                } else {
                    System.out.println("無效外部請求:樓層 " + floor + " 超出範圍。");
                }
            } catch (NumberFormatException e) {
                System.out.println("無效外部請求格式:" + input + ",樓層需為整數。");
            } catch (IllegalArgumentException e) {
                System.out.println("無效外部請求格式:" + input + ",方向需為 UP 或 DOWN。");
            }
        } else {
            System.out.println("無效請求格式:" + input);
        }
    }

    private boolean isFloorValid(int floor) {
        return floor >= lift.getMinFloor() && floor <= lift.getMaxFloor();
    }
}

2.4.3 核心分析

  • 關鍵改進
  1. 調度算法優化determineNextDestination 方法變得更加智能。它會優先處理當前方向上的所有請求,直到“盡頭”,然後才會切換方向去處理另一側的請求,這更符合真實電梯的運行邏輯。
  2. 輸入處理健壯化:通過正則表達式和 try-catch 塊,系統能夠從容應對各種格式錯誤的輸入,提高了程序的穩定性和用户體驗。

三、採坑心得:從錯誤中迭代,用數據説話

3.1 坑 1:請求去重邏輯簡陋(題集 1)

  • 問題描述:最初使用 List 存儲請求,去重時需要遍歷整個列表,代碼繁瑣且效率低下。更嚴重的是,對於外部請求,判斷“相同”的邏輯分散在 add 方法和 shouldStop 方法中,容易出錯且難以維護。
  • 解決方案:在題集 2 中,引入 Passenger 類和 Set 集合。Set 天然具備去重能力,而 Passenger 類通過重寫 equalshashCode 方法,定義了“什麼是相同的請求”。這使得去重邏輯變得異常簡單和優雅。
  • 改進結果:去重邏輯的代碼量減少了 80%,且 bug 率降為零。

3.2 坑 2:方向切換邏輯錯誤(題集 2)

  • 問題描述:在第一版調度算法中,determineNextDestination 方法總是尋找全局最近的請求,這導致電梯在運行過程中頻繁變向,形成“之”字形路線,效率極低。例如,電梯在 1 樓,有 3 樓 UP 和 5 樓 DOWN 的請求,它可能先去 3 樓,再返回 2 樓去接 5 樓下來的乘客,這顯然不合理。
  • 解決方案:在題集 3 中,重構了調度算法,嚴格遵循“同方向優先”原則。電梯一旦確定了一個方向(如 UP),就會一路向上,處理完所有上方的請求後,才會考慮向下。
  • 改進結果:電梯運行路徑變得非常“順暢”,無效的往返次數大大減少,更貼近真實世界的行為模式。

3.3 坑 3:輸入處理不健壯(題集 2)

  • 問題描述:最初的輸入處理代碼使用 String.split 和簡單的 if-else,對用户輸入的格式要求非常嚴格。一旦輸入包含多餘的空格或不符合預期的字符,程序就會因為 NumberFormatException 等異常而崩潰。
  • 解決方案:在題集 3 中,引入了正則表達式(PatternMatcher)來解析輸入。正則表達式能夠精確地匹配複雜的字符串格式,並忽略無關的空格。同時,使用 try-catch 塊捕獲所有可能發生的異常,確保程序的健壯性。
  • 改進結果:程序能夠優雅地處理各種格式錯誤的輸入,並給出清晰的提示信息,用户體驗和系統穩定性顯著提升。

四、改進建議:邁向多電梯與智能化

當前的系統已經相當完善,但仍有巨大的擴展空間:

4.1 短期改進(基於現有架構)

  1. 引入日誌框架:使用 SLF4J + Logback 替代 System.out.println,便於日誌的分級、持久化和管理。
  2. 增加單元測試:為核心類(如 RequestQueue, Lift 的調度算法)編寫單元測試,確保邏輯的正確性,併為未來的重構提供保障。
  3. 實現乘客目的樓層的動態添加:目前,外部請求的乘客目的樓層是未知的。可以模擬一個場景:當電梯在某樓層開門時,由 LiftController 再次接收輸入,為剛上車的乘客添加內部目的樓層請求。

4.2 中期改進(功能擴展)

  1. 多電梯協同調度:這是最自然的下一步。可以引入一個 ElevatorSystem 類,作為所有電梯的“總調度室”。當有新請求時,ElevatorSystem 根據某種算法(如“就近原則”、“最小等待時間”等)選擇最合適的電梯去響應。
  2. 增加載重和人數限制:為 Lift 類增加 currentLoadmaxLoad 屬性。當電梯超重時,不再響應新的外部請求,或拒絕乘客進入。

4.3 長期改進(架構升級)

  1. 分佈式系統:將 ElevatorSystem(調度中心)、Lift(電梯實例)、LiftController(多個輸入終端)分別部署為獨立的服務,通過網絡進行通信。這可以模擬一個真實的、大型建築物中的分佈式電梯控制系統。
  2. 智能化調度算法:引入更高級的調度算法,如基於遺傳算法、強化學習等 AI 技術,根據實時的人流數據預測交通高峯,提前調度電梯到繁忙區域,從而實現全局最優解。

五、總結

通過這三次題集的迭代,我們不僅實現了一個功能完備的電梯調度系統,更重要的是,我們親身體驗了一個軟件從簡陋到完善,從單體到模塊化的演進過程。

  • 技術能力提升:我們深化了對面向對象設計原則(特別是單一職責原則)的理解,掌握瞭如何使用 Set、正則表達式等工具解決實際問題,並學會了如何編寫健壯、可維護的代碼。
  • 工程化思維培養:我們學會了如何分析問題、識別瓶頸、進行重構優化,並養成了用數據和測試來驗證改進效果的習慣。
  • 問題排查能力增強:從最初面對 bug 時的手足無措,到後來能夠快速定位問題根源並給出優雅的解決方案,我們的調試和排查能力得到了極大的鍛鍊。