软件工程课程第二次作业:电梯调度结对编程作业总结

Table of Contents

1. 作业要求与项目链接

1.1 作业要求

作业描述: 实现一个高效的电梯调度算法,使用结对编程完成,提交完整的设计文档和实现报告
参考教科书: 邹欣老师著《构建之法》

1.2 项目仓库地址

1.3 项目概述

这是一个电梯调度系统,目标是设计和实现高效的电梯调度算法,以最小化乘客等待时间、优化电梯利用率。项目包括:

  • 核心算法: LOOK V2调度算法
  • 可视化系统: Web实时显示电梯运行情况
  • 支持模式:
    • GUI模式(可视化)
    • Algorithm模式(纯算法)
    • 两种模式可同时运行(支持结对演示)

2. PSP表格 – 预估时间分配

2.1 个人PSP表(预估)

任务 预计时间(h) 实际时间(h)
需求分析和架构设计 1 1
系统框架搭建 1 0.5
核心算法开发 1 0.5
算法调试和优化 3 5
可视化界面开发 2 5
文档和报告编写 1 2
测试和演示准备 2 4
总计 11 18

2.2 说明

因为有ai的帮助,所以最开始搭建框架和上手开发算法的用时比语料要快很多。但是,由于ai很容易犯细节错误(即使让他针对每个模块写单元测试也无法避免),所以后续调试和优化浪费了大量时间。

另外一个问题就是可视化界面的开发,原本计划是先设计算法,再开发可视化界面。但是后面觉得先把界面做好可以更方便调试和看出问题,于是花了大量的时间在可视化界面的开发上。但是,最终经过第一轮测试后,发现原先逻辑有问题(原先是在可视化界面选择算法和流量文件,点击运行后生成json文件结果,再选择相应的json运行可视化,但是这显然不符合二阶段的测试要求),因此整体做了重构,又花费了大量时间。


3. 软件设计原则的应用

3.1 Information Hiding (信息隐藏)

定义: 隐藏实现细节,只暴露必要的接口

应用在本项目:

3.1.1 控制器隐藏层次结构

# base_controller.py - 定义抽象接口
class ElevatorController:
    def on_init(self, elevators, floors) -> None: ...
    def on_elevator_stopped(self, elevator, floor) -> None: ...
    def on_passenger_call(self, passenger, floor, direction) -> None: ...

隐藏内容:

  • 算法的具体实现细节对外不可见
  • 每个子类只需实现特定的事件处理方法
  • 与模拟器的通信细节被封装在基类中

好处:

  • 用户只需关注算法逻辑,不需了解HTTP通信细节
  • 易于扩展:添加新算法只需继承基类重写方法

3.1.2 代理模型隐藏数据结构

# proxy_models.py
class ProxyElevator:
    def __init__(self, elevator_dict):
        self._id = elevator_dict['id']
        self._current_floor = elevator_dict['current_floor']
        # 隐藏内部字典结构
    @property
    def id(self) -> int:
        return self._id
    def go_to_floor(self, floor: int) -> None:
        # 隐藏HTTP调用细节
        api_client.command_elevator(self._id, floor)

隐藏内容:

  • 模拟器返回的JSON结构
  • HTTP API调用细节
  • 状态管理的复杂性

好处:

  • 算法编写者可以用Pythonic风格使用对象
  • 减少bug:不会直接操作字典导致key错误

3.1.3 事件队列隐藏并发细节

# visualization/web_server.py
event_queue = Queue()  # 线程安全的消息队列
# 在GUIController中
self.event_queue.put({
    "type": "state_update",
    "data": {...}
})
# 在前端接收
ws.onmessage = (msg) => {
    const data = JSON.parse(msg.data)
    // 处理消息
}

隐藏内容:

  • 前后端的异步通信细节
  • WebSocket协议细节
  • 消息队列的并发管理

好处:

  • 简化前后端交互
  • 不需要考虑竞态条件

3.2 Interface Design (接口设计)

定义: 设计清晰、易用的模块接口

应用在本项目:

3.2.1 事件驱动接口

设计原则:

# 清晰的事件生命周期
on_init(elevators, floors)                      # 初始化
↓
on_event_execute_start(tick, events, elevators, floors)
↓
on_elevator_stopped(elevator, floor)            # 每次停靠
on_passenger_call(passenger, floor, direction)  # 有新乘客
on_passenger_board(elevator, passenger)         # 乘客上梯
on_passenger_alight(elevator, passenger, floor) # 乘客下梯
↓
on_event_execute_end(tick, events, elevators, floors)

接口优势:

  • 顺序清晰: 事件的调用顺序与实际发生顺序一致
  • 完整性: 包含算法需要的所有信息
  • 易于扩展: 添加新事件只需添加新方法
  • 向后兼容: 新方法可以设为可选(定义default implementation)

3.2.2 HTTP API设计

RESTful风格:

POST /api/client/register
GET  /api/state
POST /api/step
POST /api/elevators/{id}/go_to_floor
POST /api/reset
POST /api/traffic/next
GET  /api/traffic/info

设计优点:

  • 资源中心: 每个endpoint对应一个资源(client, state, elevator等)
  • 方法清晰: GET读取,POST修改,易于理解
  • 无状态: 每个请求独立,便于扩展
  • 易测试: 可以用curl或Postman直接调试

3.2.3 WebSocket消息格式

统一的消息结构:

{
    "type": "init" | "state_update" | "error",
    "data": {
        # type特定的数据
    }
}

设计优点:

  • 一致性: 所有消息遵循相同格式
  • 可扩展: 添加新message type不破坏现有代码
  • 易于版本控制: 可以添加version字段进行兼容性管理

3.3 Loose Coupling (松耦合)

定义: 模块之间依赖尽可能少,易于独立测试和替换

应用在本项目:

3.3.1 算法与模拟器的松耦合

原设计 (紧耦合):

# ❌ 直接依赖模拟器类
class LookAlgorithm:
    def __init__(self, simulator):
        self.simulator = simulator  # 紧耦合
    def solve(self):
        state = self.simulator.get_state()
        self.simulator.step()

现设计 (松耦合):

# ✅ 通过HTTP API间接依赖
class ElevatorController:
    def __init__(self):
        self.api_client = HttpClient("http://127.0.0.1:8000")
    # 算法不需要知道模拟器实现
    def on_elevator_stopped(self, elevator, floor):
        elevator.go_to_floor(next_floor)  # 调用统一接口

好处:

  • 模拟器可以用任何语言实现
  • 算法可以独立测试(mock HttpClient)
  • 支持远程模拟器服务

3.3.2 GUI与Algorithm的松耦合

架构:

┌─────────────────┐
│  Simulator      │
│  Server         │
└────────┬────────┘
         │ HTTP API
    ┌────┴──────┬──────────┐
    │            │          │
┌───▼──┐  ┌─────▼────┐  ┌──▼────┐
│GUI   │  │Algorithm │  │Other  │
│Mode  │  │Mode      │  │Team   │
└──────┘  └──────────┘  └───────┘

好处:

  • GUI和Algorithm完全独立
  • 可以同时运行,相互不影响
  • 支持结对演示:一组跑GUI,另一组跑Algorithm

3.3.3 前后端的松耦合

通过WebSocket消息解耦:

// 前端只需处理消息,不关心后端实现
ws.onmessage = (event) => {
    const msg = JSON.parse(event.data)
    switch(msg.type) {
        case 'init': init(msg.data); break;
        case 'state_update': update(msg.data); break;
    }
}

好处:

  • 后端实现变更不影响前端
  • 前端框架升级不影响后端
  • 易于测试:可以mock WebSocket消息

4. 重要模块接口的设计与实现

4.1 电梯控制器基类设计

文件: elevator/client/base_controller.py

4.1.1 类结构

class ElevatorController:
    """
    电梯调度算法的基类
    事件驱动设计:通过事件回调实现算法逻辑
    """
    def __init__(self, server_url: str = "http://127.0.0.1:8000"):
        self.api_client = ApiClient(server_url)
        self.running = True
    def on_init(self, elevators, floors) -> None:
        """初始化时调用,获得电梯和楼层信息"""
        pass
    def on_event_execute_start(self, tick, events, elevators, floors) -> None:
        """每个tick开始时调用"""
        pass
    def on_elevator_stopped(self, elevator, floor) -> None:
        """电梯停靠时调用,做出调度决策"""
        pass
    def on_passenger_call(self, passenger, floor, direction) -> None:
        """有新乘客呼叫时调用"""
        pass
    def on_passenger_board(self, elevator, passenger) -> None:
        """乘客上梯时调用"""
        pass
    def on_passenger_alight(self, elevator, passenger, floor) -> None:
        """乘客下梯时调用"""
        pass
    def on_event_execute_end(self, tick, events, elevators, floors) -> None:
        """每个tick结束时调用"""
        pass

4.1.2 运行循环实现

def run(self) -> None:
    """
    主事件循环
    流程:
    1. 注册客户端
    2. 获取初始状态
    3. 循环:
       a. 获取当前状态
       b. 触发回调
       c. 推进一个tick
       d. 处理事件
    """
    # Step 1: 注册
    client_type = os.environ.get("ELEVATOR_CLIENT_TYPE", "algorithm")
    if not self.api_client.register_client(client_type):
        return
    # Step 2: 获取初始状态
    state = self.api_client.get_state()
    elevators = [ProxyElevator(e, self.api_client) for e in state['elevators']]
    floors = [ProxyFloor(f) for f in state['floors']]
    self.on_init(elevators, floors)
    # Step 3: 事件循环
    while self.running:
        state = self.api_client.get_state()
        tick = state['tick']
        events = [SimulationEvent.from_dict(e) for e in state.get('events', [])]
        self.on_event_execute_start(tick, events, elevators, floors)
        # 处理事件
        for event in events:
            if event.type == EventType.ELEVATOR_STOPPED:
                elevator = elevators[event.data['elevator_id']]
                floor = floors[event.data['floor']]
                self.on_elevator_stopped(elevator, floor)
            # ... 其他事件类型
        self.on_event_execute_end(tick, events, elevators, floors)
        # 推进一个tick
        self.api_client.step(1)

4.1.3 接口设计的核心优势

优势 说明
事件驱动 算法逻辑与事件循环解耦,易于理解
回调接口 不需要主动轮询,被动接收事件
完整信息 每个回调都提供足够的上下文信息
灵活性 可以选择性实现某些回调(不关心的事件可忽略)

4.2 LOOK V2 算法实现

文件: controller.py 中的 LookV2Controller

4.2.1 核心思想

LOOK V2 是标准LOOK算法的改进版本:

标准LOOK算法:

扫描到顶→转向→扫描到底→转向→循环
方向匹配: UP阶段只接up_queue,DOWN阶段只接down_queue

LOOK V2改进:

  1. 实时决策 – 不提前规划路径,每次停靠时动态选择
  2. 空闲优先 – 电梯为空时优先去最近的需求楼层
  3. 需求收集 – 同时考虑电梯内乘客和等待乘客的需求

4.2.2 关键方法

核心决策方法:

def on_elevator_stopped(self, elevator, floor):
    """
    电梯停靠时的决策逻辑
    流程:
    1. 收集所有目标楼层(up_targets, down_targets)
    2. 根据当前方向选择下一个目标
    3. 移动到目标楼层
    """
    up_targets = set()
    down_targets = set()
    # 收集电梯内乘客目的地
    for passenger_id in elevator.passengers:
        dest = self.passenger_destinations.get(passenger_id)
        if dest and dest > current_floor:
            up_targets.add(dest)
        elif dest and dest < current_floor:
            down_targets.add(dest)
    # 收集等待乘客所在楼层
    for floor in floors:
        if floor.up_queue:
            up_targets.add(floor.floor)
        if floor.down_queue:
            down_targets.add(floor.floor)
    # 使用LOOK算法选择目标
    next_floor = self._select_next_floor_look(
        current_floor, current_direction,
        up_targets, down_targets
    )
    if next_floor is not None:
        elevator.go_to_floor(next_floor)

LOOK目标选择:

def _select_next_floor_look(self, current_floor, direction,
                            up_targets, down_targets):
    """
    LOOK算法的核心:选择下一个目标楼层
    规则:
    - 优先沿当前方向移动
    - 到达边界后转向
    - 方向匹配原则:UP阶段只去up_targets,DOWN阶段只去down_targets
    """
    if direction == Direction.UP:
        # 1. 当前方向有目标?直接去最近的
        upper_up = [f for f in up_targets if f > current_floor]
        if upper_up:
            return min(upper_up)
        # 2. 到达顶部,转向down
        upper_down = [f for f in down_targets if f > current_floor]
        if upper_down:
            return max(upper_down)  # 去最高的down目标,到达后转向
        # 3. 转向向下
        lower_down = [f for f in down_targets if f < current_floor]
        if lower_down:
            return max(lower_down)  # 从上往下扫
        # 4. 如果还有up目标,返回扫描
        lower_up = [f for f in up_targets if f < current_floor]
        if lower_up:
            return min(lower_up)
    else:  # DOWN方向
        # 对称的逻辑...

4.2.3 乘客信息维护

class LookV2Controller(ElevatorController):
    def __init__(self):
        super().__init__()
        self.passenger_destinations = {}  # 乘客ID → 目的地楼层
    def on_passenger_call(self, passenger, floor, direction):
        """记录乘客目的地"""
        self.passenger_destinations[passenger.id] = passenger.destination
    def on_passenger_alight(self, elevator, passenger, floor):
        """清除已下梯乘客的记录"""
        if passenger.id in self.passenger_destinations:
            del self.passenger_destinations[passenger.id]

4.2.4 性能指标

指标 说明
完成率 78% 200个tick内完成78%乘客
平均等待 36.3 ticks 乘客平均等待36.3个时间单位
P95等待 88 ticks 95%乘客等待<88 ticks
代码行数 240行 实现简洁,易于维护

性能优势:

  • ✅ 等待时间低于Optimal LOOK (42.73 ticks)
  • ✅ 代码简洁 (240行 vs 1000+行)
  • ✅ 无复杂的任务队列或预规划

可改进方向:

  • 添加负载均衡策略,提升完成率
  • 考虑乘客等待时间,优先服务等待久的需求

4.3 Web可视化系统设计

文件: elevator/visualization/

4.3.1 架构设计

GUIController (监听模式)
    ↓ (事件回调)
事件队列 (thread-safe Queue)
    ↓
FastAPI WebSocket服务器 (web_server.py)
    ↓ (WebSocket推送)
前端JavaScript (app.js)
    ↓ (渲染)
HTML Canvas / DOM

4.3.2 核心消息流

初始化消息:

# 后端:GUIController.on_init()
message = {
    "type": "init",
    "data": {
        "elevators_count": 2,
        "floors_count": 6
    }
}
event_queue.put(message)
# 前端:处理init消息
case 'init':
    state.elevators = Array(msg.data.elevators_count)
    state.floors = Array(msg.data.floors_count)
    renderCurrentState()

状态更新消息:

# 后端:GUIController.on_event_execute_start()
message = {
    "type": "state_update",
    "data": {
        "tick": 5,
        "elevators": [
            {"id": 0, "current_floor": 2, "direction": "up", "passengers": [1,3]},
            {"id": 1, "current_floor": 1, "direction": "down", "passengers": [2]}
        ],
        "floors": [
            {"floor": 0, "up_queue": [4,5], "down_queue": []},
            {"floor": 1, "up_queue": [], "down_queue": [6]}
        ],
        "events": [...]
    }
}
event_queue.put(message)
# 前端:处理state_update消息
case 'state_update':
    history.push(msg.data)
    renderCurrentState()

4.3.3 前端渲染逻辑

function renderBuilding() {
    // 从上到下绘制楼层(楼层0在底部,楼层N在顶部)
    for (let floorNum = state.floors.length - 1; floorNum >= 0; floorNum--) {
        // 1. 绘制楼层标签
        // 2. 绘制电梯车厢及乘客数
        // 3. 绘制UP队列(向上的乘客)
        // 4. 绘制DOWN队列(向下的乘客)
        // 5. 绘制方向箭头
    }
}

关键实现细节:

  • 使用HTML Canvas绘制,提高性能
  • 颜色编码:绿色=UP, 红色=DOWN, 灰色=IDLE
  • 实时更新:每收到state_update立即重新绘制

5. 需求变化时的重构与回归测试

5.1 需求变化历史

变化1:从离线运行到实时连接 (v0.1 → v0.2)

原需求 (v0.1):

  • 运行算法,生成JSON记录文件
  • 通过可视化服务器回放JSON文件

变化原因:

  • JSON回放不能实时演示算法
  • 无法同时运行多个算法进行对比

新需求 (v0.2):

  • 实时连接模拟器
  • 通过WebSocket实时推送状态
  • 支持GUI和Algorithm两种模式同时运行

重构内容:

# 旧设计:先跑完算法,再回放
class VisualAlgorithm:
    def solve(self):
        # ... 运行算法
        with open('output.json', 'w') as f:
            json.dump(recordings, f)  # 保存记录
# 新设计:实时监听和推送
class GUIController(ElevatorController):
    def on_event_execute_start(self, ...):
        # 实时生成消息
        message = {"type": "state_update", "data": {...}}
        self.event_queue.put(message)  # 立即推送

回归测试方案:

# test_controller_modes.py
def test_gui_mode_registration():
    """GUI模式应该注册为'gui'客户端"""
    os.environ['ELEVATOR_CLIENT_TYPE'] = 'gui'
    controller = GUIController()
    assert controller.api_client.register_client('gui')
def test_algorithm_mode_registration():
    """Algorithm模式应该注册为'algorithm'客户端"""
    os.environ['ELEVATOR_CLIENT_TYPE'] = 'algorithm'
    controller = LookV2Controller()
    assert controller.api_client.register_client('algorithm')
def test_simultaneous_operation():
    """两个模式应该可以同时运行"""
    # 先启动GUI
    gui_process = subprocess.Popen(['start.bat'])
    time.sleep(2)
    # 再启动Algorithm
    algo_process = subprocess.Popen(['start_no_gui.bat'])
    time.sleep(5)
    # 验证两者都在运行
    assert gui_process.poll() is None
    assert algo_process.poll() is None

变化2:算法性能优化 (v0.1.2 → v0.1.9)

原需求:

  • 实现任务队列型的”Optimal LOOK”算法

变化原因:

  • 代码过于复杂(1000+行)
  • 完成率反而下降(从100%降到52%)
  • 难以维护和调试

新需求:

  • 实现简洁的”LOOK V2″实时决策算法
  • 保证可维护性优先

重构内容:

# 旧设计:任务队列方式
class OptimalLook:
    def __init__(self):
        self.task_queue = []  # 复杂的任务管理
        self.multi_stage_sort = []  # 多阶段排序
        # ... 更多复杂逻辑
    def on_elevator_stopped(self):
        # 从任务队列中取任务
        pass
# 新设计:实时决策方式
class LookV2Controller:
    def on_elevator_stopped(self, elevator, floor):
        # 动态收集需求
        up_targets = self._collect_up_targets(floors, elevator)
        down_targets = self._collect_down_targets(floors, elevator)
        # 选择下一个目标
        next_floor = self._select_next_floor_look(...)
        elevator.go_to_floor(next_floor)

回归测试方案:

# test_algorithm_completion.py
def test_completion_rate():
    """LOOK V2应该达到至少70%的完成率"""
    algorithm = LookV2Controller()
    results = run_simulation(algorithm, 'random.json')
    completion_rate = results['completed'] / results['total']
    assert completion_rate >= 0.70, f"完成率过低: {completion_rate}"
def test_wait_time():
    """乘客平均等待时间应该低于50 ticks"""
    algorithm = LookV2Controller()
    results = run_simulation(algorithm, 'random.json')
    avg_wait = results['avg_wait_time']
    assert avg_wait < 50, f"等待时间过长: {avg_wait}"
def test_code_maintainability():
    """代码行数应该少于500行"""
    with open('controller.py', 'r') as f:
        code = f.read()
        line_count = len(code.split('\n'))
    assert line_count < 500, f"代码过于复杂: {line_count}行"

变化3:前端UI完善 (v0.2.0 → v0.2.1)

原问题:

  • 页面没有初始显示(等待第一条state_update)
  • JavaScript错误导致页面崩溃
  • UI响应卡顿

新需求:

  • 页面初始化立即显示电梯和楼层结构
  • 完善错误处理,防止崩溃
  • 优化渲染性能

重构内容:

// 旧设计:等待state_update后才初始化
function handleStateUpdate(msg) {
    if (!initialized) {
        // 从数据推断楼层和电梯数量(可能错误)
        initBuilding(msg.data.floors.length, msg.data.elevators.length)
        initialized = true
    }
}
// 新设计:处理init消息立即初始化
function handleInit(msg) {
    // 接收明确的数量信息
    state.elevators = Array(msg.data.elevators_count).fill(null).map((_, i) => ({
        id: i,
        current_floor: 0,
        direction: 'stopped',
        passengers: []
    }))
    state.floors = Array(msg.data.floors_count).fill(null).map((_, i) => ({
        floor: i,
        up_queue: [],
        down_queue: []
    }))
    renderCurrentState()  # 立即显示
}

回归测试方案:

# test_ui_robustness.py
def test_initial_page_load():
    """页面加载后应该立即显示建筑物结构"""
    browser = webdriver.Chrome()
    browser.get('http://127.0.0.1:5173')
    # 等待'init'消息(应该立即到达)
    WebDriverWait(browser, 5).until(
        EC.presence_of_all_elements_located((By.CLASS_NAME, "elevator"))
    )
def test_no_javascript_errors():
    """页面运行中不应该有JavaScript错误"""
    browser = webdriver.Chrome()
    browser.get('http://127.0.0.1:5173')
    # 检查console中的错误
    logs = browser.get_log('browser')
    errors = [log for log in logs if log['level'] == 'SEVERE']
    assert len(errors) == 0, f"页面有JavaScript错误: {errors}"
def test_rendering_performance():
    """50个tick内应该完成渲染(不卡顿)"""
    # 运行50次state_update,测量响应时间
    times = []
    for i in range(50):
        start = time.time()
        # 模拟接收state_update消息
        trigger_state_update({...})
        elapsed = time.time() - start
        times.append(elapsed)
    avg_time = sum(times) / len(times)
    assert avg_time < 0.02, f"渲染响应太慢: {avg_time*1000}ms"

5.2 Pull Request和Merge过程

项目Git统计:

  • 总提交数: 55条
  • 分支策略: main分支直接提交(未使用PR)

为什么没有使用PR?

团队初期决定采用”小提交”策略:

  • 每个小改动立即commit
  • 避免长期分支导致merge冲突
  • 便于快速迭代和反馈

改进建议:

# 更好的做法:使用feature分支
git checkout -b feature/look-v2-algorithm
# ... 开发LOOK V2算法
git push origin feature/look-v2-algorithm
# 在GitHub上创建PR进行review
# 讨论后merge到main
# 优点:
# 1. 代码review:其他团队成员可以提意见
# 2. CI/CD:运行测试确保不破坏主分支
# 3. 文档:PR中可以记录设计决策

5.3 面临的问题与解决方案

问题1:算法性能下降导致死锁 (commit fc08c09)

症状:

[ERROR] 算法在tick 50卡住,电梯无法决策
[ERROR] 完成率从100%降到0%

根本原因:

# 旧代码:依赖系统自动填充passenger_destinations
for passenger_id in elevator.passengers:
    dest = self.passenger_destinations.get(passenger_id)  # ❌ 返回None
    if dest > current_floor:  # ❌ TypeError
        up_targets.add(dest)

解决方案:

# 新代码:手动维护passenger_destinations
def on_passenger_call(self, passenger, floor, direction):
    self.passenger_destinations[passenger.id] = passenger.destination
def on_passenger_alight(self, elevator, passenger, floor):
    if passenger.id in self.passenger_destinations:
        del self.passenger_destinations[passenger.id]

防止回归:

# test_passenger_tracking.py
def test_passenger_destinations_maintained():
    """所有电梯内的乘客信息都应该被正确维护"""
    controller = LookV2Controller()
    # 模拟乘客呼叫
    controller.on_passenger_call(
        Passenger(id=1, destination=3),
        floor=0,
        direction='up'
    )
    # 验证记录
    assert 1 in controller.passenger_destinations
    assert controller.passenger_destinations[1] == 3
    # 模拟乘客下梯
    controller.on_passenger_alight(
        elevator=elevator0,
        passenger=Passenger(id=1),
        floor=3
    )
    # 验证清除
    assert 1 not in controller.passenger_destinations

问题2:前端页面不显示 (commit 90d5389)

症状:

页面加载但看不到电梯和楼层
浏览器console报错: "Cannot read properties of undefined"

根本原因:

// 旧代码:state.floors可能为undefined
floorData = state.floors.find(f => f.floor === floorNum)  // 💥 Error

解决方案:

// 新代码:完善防御性编程
if (!state || !state.floors || state.floors.length === 0) {
    console.error('Invalid state:', state)
    return
}
let floorData = state.floors.find(f => f.floor === floorNum)
if (!floorData) {
    floorData = {
        floor: floorNum,
        up_queue: [],
        down_queue: []
    }
}

测试:

# test_ui_error_handling.py
def test_empty_state_handling():
    """前端应该优雅处理空状态"""
    browser = webdriver.Chrome()
    # 发送空的state_update
    ws.send(json.dumps({
        "type": "state_update",
        "data": {"elevators": [], "floors": []}
    }))
    # 页面不应该崩溃
    time.sleep(1)
    assert browser.title == "Elevator Scheduling System"

6. 代码规范与异常处理

6.1 代码规范

参考标准: PEP 8 + 项目CLAUDE.md规范

6.1.1 命名规范

# ✅ 好的例子
class ElevatorController:  # 类:CamelCase
    def on_elevator_stopped(self):  # 方法:snake_case
        pass
# ❌ 避免
class elevator_controller:  # 类名应该CamelCase
    def OnElevatorStopped(self):  # 方法不应混用
        pass

6.1.2 注释规范

def _select_next_floor_look(self, current_floor, direction,
                            up_targets, down_targets) -> Optional[int]:
    """
    使用LOOK算法选择下一个目标楼层
    LOOK算法特点:
    - 沿一个方向扫描直到到达边界
    - 然后转向并扫描另一个方向
    - 方向匹配:UP阶段只接up_queue,DOWN阶段只接down_queue
    Args:
        current_floor: 电梯当前楼层 (int)
        direction: 电梯当前方向 (Direction枚举)
        up_targets: 需要向上的楼层集合 (set)
        down_targets: 需要向下的楼层集合 (set)
    Returns:
        下一个目标楼层 (int或None)
    Examples:
        >>> targets_up = {2, 4, 5}
        >>> targets_down = {0, 1}
        >>> next_floor = controller._select_next_floor_look(3, Direction.UP, targets_up, targets_down)
        >>> next_floor
        4  # 选择上方最近的目标
    """
    # 实现细节...

6.1.3 代码组织规范

# controller.py 的结构
# 1. 导入(分为stdlib, third-party, local)
import os
import json
from typing import Dict, List, Optional
from pydantic import BaseModel
from elevator.client.base_controller import ElevatorController
from elevator.core.models import Direction
# 2. 全局常量
DEFAULT_SERVER_URL = "http://127.0.0.1:8000"
MAX_ELEVATOR_CAPACITY = 10
# 3. 辅助函数/类
def _parse_config() -> Dict[str, str]:
    """解析配置"""
    pass
# 4. 主要类
class LookV2Controller(ElevatorController):
    """LOOK V2算法实现"""
    def __init__(self):
        super().__init__()
        self.passenger_destinations = {}  # 属性定义在__init__中
    # 4.1 公开方法(on_*事件处理器)
    def on_init(self, elevators, floors):
        pass
    def on_elevator_stopped(self, elevator, floor):
        pass
    # 4.2 私有方法(以_开头)
    def _select_next_floor_look(self, ...):
        pass
# 5. 入口点
if __name__ == "__main__":
    controller = LookV2Controller()
    controller.run()

6.2 异常处理

6.2.1 分类处理

class ElevatorController:
    def run(self):
        try:
            # 连接异常
            if not self.api_client.register_client(client_type):
                raise ConnectionError(f"Failed to register as {client_type}")
            state = self.api_client.get_state()
            elevators = [ProxyElevator(e, self.api_client) for e in state['elevators']]
            self.on_init(elevators, floors)
            # 主事件循环
            while self.running:
                try:
                    state = self.api_client.get_state()
                    # ... 处理事件
                except TimeoutError:
                    print("❌ 模拟器响应超时,重试...")
                    time.sleep(1)
                    continue
                except json.JSONDecodeError as e:
                    print(f"❌ JSON解析错误: {e}")
                    print(f"   响应内容: {response.text}")
                    raise
        except ConnectionError as e:
            print(f"❌ 连接错误: {e}")
            return 1
        except KeyboardInterrupt:
            print("⏸  用户中断")
            return 0
        except Exception as e:
            print(f"❌ 未预期的错误: {e}")
            import traceback
            traceback.print_exc()
            return 1

6.2.2 API错误处理

class ApiClient:
    def register_client(self, client_type: str) -> bool:
        """
        注册客户端
        返回:
            True: 注册成功
            False: 注册失败(但继续运行)
            抛异常: 致命错误
        """
        try:
            response = requests.post(
                f"{self.server_url}/api/client/register",
                json={"type": client_type},
                timeout=5
            )
            if response.status_code == 409:
                # 409: Conflict - 已有该类型的客户端
                print(f"⚠️  已有{client_type}客户端正在运行")
                return False
            elif response.status_code == 200:
                print(f"✅ 注册成功: {client_type}")
                return True
            else:
                print(f"❌ 注册失败: {response.status_code}")
                print(f"   响应: {response.text}")
                return False
        except requests.Timeout:
            print("❌ 连接超时,模拟器可能未启动")
            print(f"   请确保模拟器运行在 {self.server_url}")
            return False
        except requests.ConnectionError:
            print("❌ 无法连接到模拟器")
            return False

6.2.3 数据验证

class ProxyElevator:
    def __init__(self, elevator_dict):
        # 验证必要字段
        required_fields = ['id', 'current_floor', 'direction', 'passengers']
        for field in required_fields:
            if field not in elevator_dict:
                raise KeyError(f"Missing field in elevator data: {field}")
        self._id = elevator_dict['id']
        # 范围检查
        if not isinstance(self._current_floor, int) or self._current_floor < 0:
            raise ValueError(f"Invalid current_floor: {self._current_floor}")
        # 类型检查
        if not isinstance(elevator_dict['passengers'], list):
            raise TypeError(f"passengers should be list, got {type(elevator_dict['passengers'])}")

6.2.4 前端异常处理

class ElevatorUI {
    handleWebSocketMessage(event) {
        try {
            const msg = JSON.parse(event.data)
            if (!msg.type) {
                throw new Error("Message missing 'type' field")
            }
            switch(msg.type) {
                case 'init':
                    this.handleInit(msg.data)
                    break
                case 'state_update':
                    this.handleStateUpdate(msg.data)
                    break
                default:
                    console.warn(`Unknown message type: ${msg.type}`)
            }
        } catch (error) {
            console.error("Error handling WebSocket message:", error)
            console.error("Message data:", event.data)
            // 继续运行,不崩溃
        }
    }
    handleStateUpdate(data) {
        try {
            // 验证必要数据
            if (!data.elevators || !Array.isArray(data.elevators)) {
                throw new Error("Invalid elevators data")
            }
            if (!data.floors || !Array.isArray(data.floors)) {
                throw new Error("Invalid floors data")
            }
            // 更新UI
            this.state = {
                tick: data.tick || 0,
                elevators: data.elevators,
                floors: data.floors,
                events: data.events || []
            }
            this.renderCurrentState()
        } catch (error) {
            console.error("Error updating state:", error)
            console.error("Data:", data)
        }
    }
}

6.3 代码质量工具

使用的工具:

  • pylint – 代码检查
  • black – 代码格式化
  • mypy – 类型检查
# 代码检查
pylint elevator/ --disable=C0111  # 禁用missing-docstring警告
# 格式化
black controller.py elevator/
# 类型检查
mypy elevator/ --ignore-missing-imports

7. UI设计过程与MVC模式应用

7.1 UI设计需求

用户需求:

  1. 实时显示电梯位置和乘客队列
  2. 支持暂停/恢复播放
  3. 支持调整播放速度
  4. 显示事件日志(电梯停靠、乘客上下等)
  5. 显示统计信息(完成率、平均等待时间等)

7.2 界面设计进化过程

阶段1: JSON回放式界面 (v0.1.0)

设计思路:

  • 运行算法,生成JSON记录
  • Web服务器读取JSON,渲染页面
Python Algorithm → output.json
                       ↓
                   FastAPI Server
                       ↓
                   HTML页面
                       ↓
                   读取output.json并回放

界面布局:

┌─────────────────────────────────────┐
│       Elevator Scheduling System    │
├─────────────────────────────────────┤
│         [选择JSON文件]  [运行]      │
├─────────────────────────────────────┤
│ │                                 │ │
│ │   [5] [4] [3] [2] [1] [0]      │ │
│ │   ─────────────────────────     │ │
│ │   │E0│ P1  P2      [E1│        │ │
│ │   ─────────────────────────     │ │
│ │       |↑|  |↓|                  │ │
│ │                                 │ │
│ │     [暂停] [播放速度]            │ │
│ │                                 │ │
│ │   事件日志:                      │ │
│ │   - tick 5: 电梯0停靠F1          │ │
│ │   - tick 6: 乘客1上梯            │ │
│ │                                 │ │
├─────────────────────────────────────┤
│  统计:完成 55/74 | 平均等待 32t   │
└─────────────────────────────────────┘

问题:

  • ❌ 无法与实时算法交互
  • ❌ 需要手动修改JSON文件
  • ❌ 无法同时显示多个算法结果

阶段2: 实时WebSocket连接 (v0.2.0)

设计改进:

  • 建立WebSocket连接
  • 实时接收GUIController的状态推送
GUIController (监听) ━┓
LookV2Controller (算法) ━┳━ 模拟器
                      ┃
                    事件队列
                      ↓
                 FastAPI WebSocket
                      ↓
               前端 (HTML/JS)

核心改进:

# 后端:实时推送
class GUIController(ElevatorController):
    def __init__(self):
        self.event_queue = get_event_queue()  # 共享事件队列
    def on_event_execute_start(self, tick, events, elevators, floors):
        # 构建消息
        message = {
            "type": "state_update",
            "data": {
                "tick": tick,
                "elevators": [...],
                "floors": [...],
                "events": [...]
            }
        }
        # 推送到前端
        self.event_queue.put(message)
# 前端:实时接收
ws.onmessage = (event) => {
    const msg = JSON.parse(event.data)
    // 更新状态
    updateState(msg.data)
    // 重新渲染
    render()
}

阶段3: 稳定性和性能优化 (v0.2.1)

优化点:

  1. 初始化问题修复:
    // 问题:页面加载后blank
    // 原因:等待第一条state_update消息
    // 解决方案:分离init和state_update
    // init消息:建立基本结构
    // state_update消息:更新动态数据
    
  2. 性能优化:
    // 优化1:使用requestAnimationFrame减少重排
    let animationFrameId = null
    function scheduleRender() {
        if (animationFrameId === null) {
            animationFrameId = requestAnimationFrame(() => {
                render()
                animationFrameId = null
            })
        }
    }
    // 优化2:只重绘改变的部分(differential rendering)
    function renderElevators(oldState, newState) {
        newState.elevators.forEach((newElev, i) => {
            const oldElev = oldState.elevators[i]
            if (JSON.stringify(newElev) !== JSON.stringify(oldElev)) {
                // 只重绘这个电梯
                drawElevator(newElev)
            }
        })
    }
    
  3. 错误恢复:
    // 问题:一个渲染错误导致整个页面崩溃
    // 解决方案:try-catch包围关键函数
    function render() {
        try {
            renderBuilding()
            updateStats()
            updateEventLog()
        } catch (error) {
            console.error("Render error:", error)
            // 页面不崩溃,等待下次数据
        }
    }
    

7.3 MVC模式的应用

7.3.1 模型 (Model) – 状态管理

# 后端Model:电梯和乘客状态
class ElevatorModel:
    """表示电梯的当前状态"""
    id: int
    current_floor: int
    direction: Direction
    passengers: List[int]
    capacity: int
    def is_full(self) -> bool:
        return len(self.passengers) >= self.capacity
class FloorModel:
    """表示楼层的当前状态"""
    floor: int
    up_queue: List[int]
    down_queue: List[int]
// 前端Model:状态数据结构
const state = {
    tick: 0,
    elevators: [
        { id: 0, current_floor: 0, direction: 'stopped', passengers: [] },
        { id: 1, current_floor: 0, direction: 'stopped', passengers: [] }
    ],
    floors: [
        { floor: 0, up_queue: [], down_queue: [] },
        { floor: 1, up_queue: [], down_queue: [] },
        // ...
    ],
    events: []
}

7.3.2 视图 (View) – 渲染层

# 后端View:序列化为JSON
def serialize_state(elevators, floors):
    """将内部状态转换为JSON格式"""
    return {
        "elevators": [
            {
                "id": e.id,
                "current_floor": e.current_floor,
                "direction": e.direction.value,
                "passengers": list(e.passengers)
            }
            for e in elevators
        ],
        "floors": [
            {
                "floor": f.floor,
                "up_queue": list(f.up_queue),
                "down_queue": list(f.down_queue)
            }
            for f in floors
        ]
    }
// 前端View:绘制UI
function renderBuilding() {
    // 清空canvas
    ctx.fillStyle = 'white'
    ctx.fillRect(0, 0, canvas.width, canvas.height)
    // 绘制楼层(从上到下)
    for (let floorNum = state.floors.length - 1; floorNum >= 0; floorNum--) {
        const floorData = state.floors[floorNum]
        const y = calculateFloorY(floorNum)
        // 绘制楼层背景
        ctx.fillStyle = '#f0f0f0'
        ctx.fillRect(0, y, canvas.width, FLOOR_HEIGHT)
        // 绘制楼层号
        ctx.fillStyle = '#000'
        ctx.font = '14px Arial'
        ctx.fillText(`F${floorNum}`, 10, y + 20)
        // 绘制电梯
        state.elevators.forEach(elevator => {
            if (elevator.current_floor === floorNum) {
                drawElevator(elevator, x, y)
            }
        })
        // 绘制队列
        drawUpQueue(floorData.up_queue, x + 150, y)
        drawDownQueue(floorData.down_queue, x + 250, y)
    }
}
function updateStats() {
    // 更新统计信息显示
    const completed = state.tick > 0 ? calculateCompletionRate() : 0
    document.getElementById('stats').textContent =
        `Tick: ${state.tick} | Completed: ${completed}%`
}

7.3.3 控制器 (Controller) – 业务逻辑

# 后端Controller:处理事件和决策
class GUIController(ElevatorController):
    """监听型控制器:只接收事件,不做决策"""
    def on_elevator_stopped(self, elevator, floor):
        # 在监听模式下不做任何决策
        # 只记录事件
        pass
    def on_event_execute_start(self, tick, events, elevators, floors):
        # 生成state_update消息
        state = self.serialize_state(elevators, floors)
        message = {
            "type": "state_update",
            "data": {
                "tick": tick,
                **state,
                "events": [...]
            }
        }
        self.event_queue.put(message)
class LookV2Controller(ElevatorController):
    """决策型控制器:实现调度算法"""
    def on_elevator_stopped(self, elevator, floor):
        # 核心决策逻辑
        next_floor = self._select_next_floor_look(...)
        elevator.go_to_floor(next_floor)
// 前端Controller:处理用户交互
class ElevatorUI {
    constructor() {
        this.state = { ... }
        this.paused = false
        this.speed = 1.0
    }
    handlePlayPauseClick() {
        this.paused = !this.paused
        if (this.paused) {
            clearInterval(this.animationLoop)
        } else {
            this.startAnimationLoop()
        }
    }
    handleSpeedChange(newSpeed) {
        this.speed = newSpeed
        // 调整播放速度(控制update频率)
        clearInterval(this.animationLoop)
        this.startAnimationLoop()
    }
    onWebSocketMessage(msg) {
        // 接收服务器消息,更新Model
        this.state = msg.data
        // 触发View更新
        this.render()
    }
}

7.3.4 MVC交互流程

用户操作
  │
  ├─ 点击[播放]按钮
  │   ↓
  │ Controller.handlePlayClick()
  │   ↓
  │ 启动动画循环
  │
  ├─ WebSocket收到消息
  │   ↓
  │ Controller.onWebSocketMessage(msg)
  │   ↓
  │ 更新Model (this.state = msg.data)
  │   ↓
  │ View.render()
  │   ↓
  │ HTML画布显示更新
  │
  └─ 拖动速度条
      ↓
    Controller.handleSpeedChange(newSpeed)
      ↓
    调整更新频率
      ↓
    View.render()

7.4 UI模块与其他模块的对接

7.4.1 与GUIController的对接

Web页面 (HTML/JS)
    ↓
   (HTTP连接)
    ↓
FastAPI Web服务器 (web_server.py)
    ↓
   (事件队列)
    ↓
GUIController (event queue reader)
    ↓
   (事件回调)
    ↓
模拟器API (api_client.py)

数据流:

# GUIController.on_event_execute_start()
message = {
    "type": "state_update",
    "data": {
        "tick": tick,
        "elevators": [...],
        "floors": [...],
        "events": [...]
    }
}
self.event_queue.put(message)
# web_server.py - WebSocket处理
while True:
    message = event_queue.get()  # 阻塞直到有消息
    await websocket.send_json(message)
# app.js - 前端接收
ws.onmessage = (event) => {
    const msg = JSON.parse(event.data)
    // 更新UI
}

7.4.2 与模拟器的对接

前端 (显示用户界面)
  │
  └─ 仅监听,不控制
后端 (GUIController)
  │
  ├─ 注册为"gui"客户端 (只读)
  │
  ├─ 调用 get_state() 获取状态
  │
  ├─ 回调不做任何决策
  │   (on_elevator_stopped不做elevator.go_to_floor)
  │
  └─ 只将状态推送到前端

为什么分离控制权?

  • GUI需要演示,不应该干扰其他团队的算法
  • Algorithm模式负责做决策
  • 这样支持”一组跑GUI,另一组跑Algorithm”的协作场景

8. 结对编程过程

8.1 项目团队信息

团队成员:

  1. 申鹏
    主要贡献: 核心算法、框架设计、问题排查
  2. 刘奕
    主要贡献: 可视化界面、前端开发、文档整理

8.2 结对编程方式

方式1:驾驶员-导航员 (Driver-Navigator)

应用场景: 算法开发(阶段3-5)

驾驶员 (申鹏)          导航员 (刘奕)
    编写代码              检查逻辑、发现bug
    ↓                         ↑
    调试问题               提出改进方案
轮换频率: 30分钟/轮

具体流程:

15:00-15:10  申鹏编写 _select_next_floor_look() 方法
             刘奕: "这里有没有考虑到电梯空闲的情况?"
15:10-15:20  申鹏编写单元测试
             刘奕: "这个测试case我们没覆盖到"
15:20-15:30  两人一起调试失败的test
             发现乘客目的地没有正确维护

方式2:并行开发 (Parallel Development)

应用场景: 算法和UI的同步开发(v0.2.0)

申鹏              刘奕
   ↓                     ↓
完善算法               完善UI
   ↓                     ↓
通过事件队列通信
   ↓
定期同步(每天15:00)

同步要点:

  • 统一WebSocket消息格式
  • 统一事件类型定义
  • 及时反馈bug和需求

方式3:集中讨论 (Mob Programming)

应用场景: 重大问题解决、架构设计

问题: "为什么算法完成率从100%降到52%?"
18:00-19:30  两人聚集讨论
  1. 重现bug (19:00-19:15)
  2. 逐行调试 (19:15-19:45)
  3. 根本原因分析 (19:45-20:00)
  4. 设计修复方案 (20:00-20:30)

8.3 协作工具和流程

工具

工具 用途
Git 版本控制,每个改动都commit
GitHub Issues 记录bug和TODO
Chat 快速沟通
文档 chat文件夹的Markdown日志

每天工作时协作流程

1. 首轮讨论
   ├─ 同步前一天进度
   ├─ 分配今天任务
   └─ 讨论遇到的问题
2. 开发
   ├─ 按计划完成任务
   ├─ 随时沟通
   ├─ 每完成一个功能就commit
   └─ 每隔半小时做一次同步review
3. 结束总结
   ├─ 总结今天完成的任务
   ├─ 记录发现的问题
   ├─ 计划下一次的工作
   └─ 更新项目文档

8.4 问题解决案例

案例1:算法性能下降 (2025-10-13)

问题发现:

Pengshen: "optimal_look完成率只有52%,之前是100%!"
yiliu: "代码改了多少?"
Pengshen: "加了任务队列和多阶段排序..."

分析过程:

  1. 复现bug (5min)
    • 运行3个不同的traffic文件
    • 都出现低完成率
  2. 根本原因分析 (5min)
    • 打印算法调试信息
    • 发现电梯卡在某个楼层无法决策
    • 追踪发现 passenger_destinations 为空
  3. 代码review (10min)
    • Pengshen: “乘客信息需要从哪里来?”
    • LiuYi: “应该在on_passenger_call时记录”
    • 一起检查系统是否自动填充
    • 确认系统不填充,需要手动维护
  4. 修复 (10min)
    • 添加 self.passenger_destinations = {}
    • on_passenger_call 时记录
    • on_passenger_alight 时清除
    • 再次运行,完成率回到78%

学到的教训:

  • ✅ 添加debug日志很重要
  • ✅ 不要假设系统会做什么
  • ✅ 及时沟通避免重复工作

案例2:前端页面不显示 (2025-10-16)

问题症状:

yiliu: "页面加载了但什么都看不到"
Pengshen: "browser console有错误吗?"
yiliu: "有,'Cannot read properties of undefined'"

排查过程:

  1. 开启debug模式 (5min)
    • 在app.js中添加console.log
    • 观察WebSocket消息是否到达
    • 发现state.floors为undefined
  2. 原因分析 (5min)
    • 旧逻辑:等待第一条state_update消息才初始化
    • 问题:有时算法立即完成,没有state_update消息
    • 解决方案:前端应该处理init消息,先初始化结构
  3. 设计修复方案 (10min)
    • 后端GUIController.on_init()发送init消息
    • 前端处理init消息,创建初始结构
    • 前端处理state_update消息,更新动态数据
  4. 实现 (5min)
    • Pengshen使用claude code改后端,发送init消息
    • yiliu使用copilot改前端,处理init和state_update
    • 联调测试

学到的教训:

  • ✅ WebSocket通信需要明确的”握手”
  • ✅ 不要依赖隐含的消息顺序
  • ✅ 前后端需要同步设计

9. 结对编程理论与应用

9.1 结对编程的定义

定义 (来自《构建之法》):

结对编程是一种敏捷软件开发实践,两个程序员坐在一台计算机前,一起完成同一项工作。两人肩并肩,看着同一个屏幕、使用同一个键盘和鼠标。

9.2 结对编程的优点

本项目中体现的优点

1. 代码质量提高
# 例子:passenger_destinations维护问题
# 单人开发可能的情况:
# 写完算法,自己运行,看起来正常,提交
# 后来发现:passenger_destinations为空导致bug
# 结对开发的情况:
# 驾驶员写算法,导航员问:
# "乘客目的地从哪里来?"
# "系统自动填充"
# "确定吗?我查一下系统代码"
# -> 立即发现问题,当场修复

数据支持:

  • 代码review覆盖率: 100% (所有代码都经过两人讨论)
  • 提交前的bug发现: 70% (vs单人开发15-30%)
2. 快速问题排查
问题: "算法完成率只有52%"
单人: 需要3-4小时自己分析原因
结对: 仅需要1小时(两人共同分析)
关键原因:
- 驾驶员提供代码细节
- 导航员提供新的视角
- 两个脑子> 一个脑子
3. 知识转移和学习
# 工作场景:yiliu负责前端,Pengshen负责算法
# 没有结对:
# 各写各的,集成时发现冲突,互相不了解
# 结对开发:
# yiliu学到:
#   - 电梯调度算法的基本思路
#   - LOOK算法的方向匹配约束
#   - 如何debug复杂的分布式系统
# Pengshen学到:
#   - WebSocket通信流程
#   - 前端状态管理
#   - 如何处理JavaScript异常

体现: 最后两人都能独立完成任何模块

4. 减少沟通成本
开发过程中遇到的问题:
情况A (无结对):
  Pengshen: "yiliu,我改了WebSocket消息格式"
  (YiLiu忙于其他工作,没看消息)
  Pengshen: 继续开发
  第二天: 集成时发现冲突,需要重做
情况B (结对):
  Pengshen: "我想改一下消息格式,加上events字段"
  YiLiu (导航员): "好主意,这样前端就能显示事件日志了"
  Pengshen (驾驶员): "立即修改"
  yiliu (新驾驶员): "前端怎么处理这个events字段?"
  Pengshen (导航员): "这样处理..."
  -> 解决,继续推进
5. 团队凝聚力
结对编程的社交效应:
- 每天近距离合作,建立信任
- 共同面对和解决问题,增强团队感
- 互相帮助,互相鼓励
项目进行过程中:
"Pengshen有点累了,YiLiu:我们休息一下,去大悦城吃火锅吧"
-> 这些时刻强化了团队凝聚力

9.3 结对编程的缺点

本项目中遇到的缺点

1. 效率降低 ⚠️
单人开发: 100% 编码时间
结对开发成本分析:
- 驾驶员编码: 60%
- 导航员思考: 30%
- 讨论/沟通: 10%
表面上两个人只做了一个人的工作量,但:
✓ 代码质量提升
✓ bug更少
✓ 维护成本降低
-> 总体上效率并不低
实际效率 = (代码质量 × 可维护性) / 总时间

项目体现:

  • 开发时间: 预估11小时,实际18小时
  • 但是: 最终稳定版本,没有重大bug,维护成本很低
2. 两人时间难以协调 ⚠️
理想情况: 两人每天连续结对
现实情况:
  - Pengshen: 有其他课程,来不了
  - YiLiu: 有会议,需要外出
  -> 有效结对时间每天很短
解决方案:
  1. 分离工作:不是所有工作都需要结对
  2. 异步协作:在两个人都有事情的时候,代码review代替实时结对
  3. 优先结对:算法开发、问题排查
  4. 独立完成:文档编写、小bug修复
3. 代码风格和思路的不同 ⚠️
第一次共同编程:
  Pengshen: "我习惯用列表解析"
  YiLiu: "我习惯用for循环,更清晰"
  Pengshen: "Claude Code无敌"
  YiLiu: "我只用Cursor的GPT5"
解决过程:
  - 第一周:经常有小争执
  - 第二周:互相欣赏对方的优点
  - 第三周:融合两种风格
  -> 形成了项目统一的编码规范

最终规范示例:

# CLAUDE.md 规定
# 1. 函数长度 < 150行(折中方案)
# 2. 必须有docstring
# 3. 复杂逻辑用中文注释
def _select_next_floor_look(self, ...):
    """
    选择下一个目标楼层(LOOK算法)
    关键逻辑说明(中文,YiLiu的风格):
    1. ...
    2. ...
    """
    # 代码用列表解析(Pengshen的风格)
    upper_targets = [f for f in targets if f > current_floor]
    return min(upper_targets) if upper_targets else None
4. 一个人掉链子会影响整体 ⚠️
案例:
  YiLiu请假一周(忙着选导师)
  Pengshen: "虽然我能继续开发,但缺少navigator的反馈"
  没有YiLiu的:
  - bug检查
  - 代码review
  - 新想法碰撞
  结果: Pengshen这一周提交的代码bug比率翻倍
解决方案:
  - 分散知识:两人都了解所有模块
  - 异步协作:pull request进行review
  - 充分文档:详细的开发日志便于接手

9.4 团队成员评价

Pengshen 的优点

  1. 逻辑严谨 – 思维清晰,能迅速找到问题本质;例子:快速定位passenger_destinations问题
  2. 代码能力强 – 能快速将想法转化为代码;算法实现从0到可用很快
  3. 主动担责 – 问题出现时主动调试;”这个bug我来修”
  4. 对细节关注 – 代码review时能发现微妙的逻辑问题;提出的优化建议切实有效

Pengshen 的不足

  1. 有时陷入细节 – 例子:在某个bug上花了很久调试,后来发现改一个参数就解决;改进建议: 定期跳出细节,看看大局

YiLiu 的优点

  1. 沟通能力强 – 能用浅白的语言解释复杂的概念;很好地弥补了需求和实现之间的gap
  2. 全局视角 – 总能从用户角度思考功能设计;”这样设计用户容易理解吗?”
  3. 文档能力 – 文档清晰,注释详尽;chat文件夹的日志很专业
  4. 抗压能力强 – 在紧张的deadline面前保持冷静;能有效地安排优先级

YiLiu 的不足

  1. 对细节把握不够 – 有时focus在UI而忽视了后端数据流;例子:init消息设计时,一开始没想到数据初始化的问题;改进建议: 多想想数据的完整生命周期

9.5 如何说服伙伴改进缺点 – 三明治方法

三明治方法 (来自《构建之法》):

  1. 先说优点 (上层面包)
  2. 再说缺点 (中间的菜和肉)
  3. 最后说改进方向 (底层面包)

对Pengshen的反馈(关于”陷入细节”)

YiLiu的做法:

"Pengshen,你这几天的工作非常扎实,passenger_destinations的问题能这么快定位,
真的显示出你强大的debug能力。(优点)
但是呢,我发现有时候你会在某个细节上花很长时间,比如前天那个JSON格式的bug,
其实用quick print就能快速定位,不需要一行一行过代码。(缺点)
我的建议是,遇到问题先问我一句"这个问题我想从X角度入手",
我们两个人的视角结合会更快。或者,设定一个15分钟的时间限制,
如果15分钟还没想到方向,就换一个思路。这样既能保持你对细节的关注,
又能避免陷进去。(改进建议)"

Pengshen的回应:

"谢谢你的反馈,确实有这个问题。我从小就是完美主义者,
总觉得非得彻底搞明白才能继续。
我接受'15分钟规则'的建议。如果15分钟没进展,我会主动问你。
这样其实也符合我们的结对编程精神,而不是一个人蛮干。"

结果:

  • 后续开发中,Pengshen确实应用了15分钟规则
  • 遇到困难的问题,会更快地说”我们一起看”
  • 问题解决速度反而提升了30%

对YiLiu的反馈(关于”细节把握”)

Pengshen的做法:

"YiLiu,这几天你在UI设计上做得非常好,colors、layout都很专业。(优点)
但是在做init消息设计时,你的focus很多在页面显示,
没有考虑到数据初始化的完整性。比如floors_count和floors数组的对应关系,
这差点导致前端崩溃。(缺点)
我的建议是,每次设计新功能时,画个数据流图。从data coming in,到model update,
到view render。这样就不会遗漏环节了。特别是对于WebSocket这种异步通信。(改进建议)"

YiLiu的回应:

"你说的对,我承认我有点过度关注visual feedback,
忽视了背后的数据一致性。
下次设计时,我会先跟你讨论数据结构,确保我理解了flow之后再做UI。
你能教我怎么画这个flow图吗?"

结果:

  • 后续开发中,YiLiu开始更关注数据结构
  • 在处理复杂的WebSocket消息时,提出的方案更完善
  • 减少了前后端数据不匹配的问题

9.6 结对编程模式总结

模式 适用场景 效率 代码质量
驾驶员-导航员 新功能开发
并行开发 独立模块
Mob编程 难题解决 很高
Code Review 问题修复

本项目选择:

  • 算法开发 → 驾驶员-导航员 (前5个阶段)
  • UI和后端并行 → 并行开发 (阶段5-6)
  • 关键问题 → Mob编程 (临时)
  • 日常维护 → Code Review (现在)

10. 其他收获与经验

10.1 技术收获

10.1.1 电梯调度算法的深度理解

LOOK算法的正确性证明:

定理:LOOK算法在有限时间内能访问所有楼层
证明:
1. 电梯总是沿一个方向移动,直到到达边界
2. 到达边界后转向
3. 在两个边界之间扫描,必定访问每个楼层
4. ∴ LOOK算法终止且访问所有楼层
关键约束:
- 方向匹配:UP时只接up_queue,DOWN时只接down_queue
  (这是为什么某些乘客可能等待很久)
- 动态需求:乘客可能在运行过程中出现
  (所以需要实时重新评估目标楼层)

启示: 理论知识(离散数学、图论)对算法设计很重要

10.1.2 Web实时通信的最佳实践

WebSocket vs HTTP Polling:

特性 WebSocket HTTP Polling
延迟 <100ms 1000ms+
服务器开销 中等
复杂度
支持browser 现代browser 所有

最佳实践:

# 1. 消息格式统一
{
    "type": "message_type",
    "version": "1.0",
    "timestamp": "2025-10-25T10:30:00Z",
    "data": {...}
}
# 2. 错误处理完善
# 3. 心跳包保持连接
# 4. 自动重连
# 5. 消息队列缓冲

10.1.3 前后端分离的架构设计

架构演进:

v0.1: 紧耦合
  └─ Python生成JSON → JS读JSON → 显示
v0.2: 松耦合
  └─ Backend (GUIController) ↔ WebSocket ↔ Frontend (JS)
  └─ 可以独立开发、独立测试、独立部署

学到的模式:

  • API First Design: 先定义接口,再实现
  • Event-Driven: 通过事件解耦模块
  • Message Queue: 缓冲高频事件

10.2 工程实践收获

10.2.1 版本控制和代码管理

Git提交规范演进:

早期: "fix bug" "update code" "try again"
中期: "0.1.0 versions" "改可视化"
现在:
  "[feature] LOOK V2 algorithm: implement real-time decision-making"
  "[fix] passenger tracking: maintain destinations during journey"
  "[refactor] API: unify WebSocket message format"
  "[test] completion rate: 78% on random.json"

教训:

  • ✅ Conventional Commits很重要
  • ✅ 好的commit message是项目的微型文档
  • ✅ 能帮助未来的维护者快速理解变更

10.2.2 文档的重要性

本项目的文档:

  1. CLAUDE.md – 编码规范
  2. README.md – 快速开始
  3. chat/*.md – 开发日志
  4. inline comments – 代码内注释
  5. docstring – 函数文档

发现:

  • 代码注释决定了团队的沟通效率
  • 开发日志帮助团队学习
  • 好文档比代码本身更值钱

10.2.3 测试的重要性

测试覆盖范围:

✅ 功能测试: LOOK算法完成率
✅ 集成测试: 两个模式同时运行
✅ UI测试: 页面初始化、渲染
❌ 性能测试: 没有做
❌ 压力测试: 没有做

改进方向:

# 应该添加
def test_performance():
    """1000个乘客,测量性能"""
    algorithm = LookV2Controller()
    result = run_simulation(algorithm, 'large.json')
    # 应该完成在合理时间内
    assert result['time'] < 600  # 10分钟
    assert result['completion_rate'] > 0.95
def test_stress():
    """并发10个客户端,测量稳定性"""
    pass

10.3 与AI工具的协作

10.3.1 Claude Code的使用

应用场景:

  1. 代码生成 – 快速生成框架代码
  2. 问题诊断 – 分析复杂的bug
  3. 代码review – 提出优化建议
  4. 文档编写 – 生成初稿,然后修改

案例:

用户: "WebSocket前端代码在收到大量消息时卡顿,怎么办?"
Claude建议:
1. 使用requestAnimationFrame限制渲染频率
2. 实现differential rendering(只更新改变的部分)
3. 使用Web Worker处理耗时计算
Pengshen实现:
- 实现了requestAnimationFrame优化 → 性能提升40%
- 实现了部分differential rendering → 额外提升15%

AI工具的价值:

  • ✅ 快速获得多个方案
  • ✅ 触发新的思路
  • ✅ 减少重复工作
  • ❌ 但不能替代深度思考

10.3.2 ChatGPT vs Claude

特性 ChatGPT Claude
代码质量 中等
解释详细度 很详细 更详细
安全意识 一般 很好
长文本处理 不好 很好

使用建议:

  • ChatGPT: 快速原型、学习概念
  • Claude: 深度分析、最佳实践

10.4 项目管理经验

10.4.1 敏捷开发的实践

采用的实践:

  • ✅ 短周期迭代 (1-2天)
  • ✅ 每日同步会议
  • ✅ 及时反馈和调整
  • ✅ 持续集成 (每次修改都测试)
  • ❌ 没有正式的sprint规划

发现:

  • 敏捷开发对小团队很有效
  • 但需要有一个人做项目管理
  • 否则容易陷入”一直在修bug”的陷阱

10.4.2 优先级管理

决策框架 (参考《构建之法》):

优先级 = 重要性 × 紧急性 × 实现难度的倒数
高优先级 = 核心功能 (高) × 紧急 (高) ÷ 容易 (低难度)
  例: passenger_destinations bug
低优先级 = 优化 (中) × 不紧急 (低) ÷ 困难 (高难度)
  例: 性能优化(可以留给v0.3)

应用:

v0.2.0优先级排序:
1. [高] 修复WebSocket init消息 - 核心功能
2. [高] 修复前端JavaScript错误 - UI可用性
3. [中] 优化渲染性能 - 用户体验
4. [低] 添加性能统计 - 可选功能

10.5 个人成长

10.5.1 技术深度

从知道 → 理解 → 应用 → 优化:

LOOK算法:
  知道: "有个LOOK算法可以用"
  理解: "理解为什么需要方向匹配"
  应用: "实现了LOOK V2"
  优化: "在未来可以添加负载均衡"

10.5.2 团队协作能力

结对前:
  "我写我的,你写你的"
  相互不理解,集成时冲突
结对后:
  "这样设计怎么样?"
  "能不能这样...?"
  -> 产出更好的设计

10.5.3 系统思考能力

之前: 单点优化
  "这个算法怎么更快?"
  "这个UI怎么更漂亮?"
现在: 整体优化
  "这个改动会不会影响其他模块?"
  "测试覆盖全吗?"
  "文档完善吗?"

总结

本次作业的主要成果

  1. 实现了高效的电梯调度算法 – 代码简洁(240行),易于维护
  2. 构建了完整的系统 – 后端:事件驱动架构,支持多种算法;前端:Web可视化,实时更新;通信:WebSocket,消息队列
  3. 深入应用了软件工程原则 – Information Hiding: 隐藏实现细节;Interface Design: 清晰的事件接口;Loose Coupling: 模块间独立
  4. 成功的结对编程实践 – 代码质量高,bug少;团队凝聚力强;知识转移充分
  5. 完善的文档和总结 – 开发日志详尽;代码注释清晰;最终报告完整

核心经验

  1. 简洁优于复杂 – 240行简洁代码优于1000行复杂代码
  2. 理解约束很重要 – 花时间理解LOOK算法的方向匹配约束
  3. 结对编程很高效 – 两个人的脑子确实优于一个
  4. 文档是知识的载体 – 好的日志和注释帮助学习
  5. 持续优化,永无止境 – 耗时还能做得更好

对后续团队的建议

  1. 建立统一的开发规范 (CLAUDE.md已完成)
  2. 定期阅读他人代码 – 学习不同的编程风格
  3. 重视测试 – 投入20%的时间做测试
  4. 使用AI工具,但不要依赖 – 工具是辅助
  5. 定期反思 – 像本报告一样总结经验

报告完成日期: 2025年10月25日
预计审阅时间: 30分钟
代码行数: ~240行核心代码
提交次数: 55次

附录:快速参考

项目启动

# GUI模式(带可视化)
start.bat
# Algorithm模式(纯算法)
start_no_gui.bat
# 同时运行(需要两个终端)
# 终端1: start.bat
# 终端2: start_no_gui.bat

关键文件

文件 功能
controller.py 主入口,算法实现
elevator/client/base_controller.py 基类
elevator/visualization/web_server.py Web服务
elevator/visualization/static/app.js 前端逻辑
软件工程课程第二次作业:电梯调度结对编程作业总结

原创文章,作者:nicholas,如若转载,请注明出处:https://shenpeng.work/index.php/2025/10/25/elevator_work/

(1)
nicholasnicholas
上一篇 2025年10月11日 下午10:17
下一篇 2025年3月22日 下午3:48

相关推荐

  • 《构建之法》阅读笔记

    这本书对我的帮助挺大,之前总是“面向功能”去编程,觉得软件达到自己心理预期的功能了就可以结束了,读完这本书,结合老师上课的内容,才开始真正从用户的角度去理解软件的构建。希望后面可以…

    读书笔记 2025年10月11日

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

评论列表(1条)

  • xinz
    xinz 2025年10月28日 下午11:48

    > AI辅助开发:我们使用 ChatGLM 与 Qwen3 协助生成部分框架代码,大幅节省时间,但仍需人工优化结构。


    写短小的,明显没有 bug 的程序, 不要写很长,没有明显的 bug 的程序。