1. 作业要求与项目链接
1.1 作业要求
作业描述: 实现一个高效的电梯调度算法,使用结对编程完成,提交完整的设计文档和实现报告
参考教科书: 邹欣老师著《构建之法》
1.2 项目仓库地址
- GitHub地址: https://github.com/Winner-Nick/Elevator
- 项目描述: Elevator Scheduling System – 实现电梯智能调度算法,支持Web可视化和算法评测
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改进:
- 实时决策 – 不提前规划路径,每次停靠时动态选择
- 空闲优先 – 电梯为空时优先去最近的需求楼层
- 需求收集 – 同时考虑电梯内乘客和等待乘客的需求
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设计需求
用户需求:
- 实时显示电梯位置和乘客队列
- 支持暂停/恢复播放
- 支持调整播放速度
- 显示事件日志(电梯停靠、乘客上下等)
- 显示统计信息(完成率、平均等待时间等)
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)
优化点:
- 初始化问题修复:
// 问题:页面加载后blank // 原因:等待第一条state_update消息 // 解决方案:分离init和state_update // init消息:建立基本结构 // state_update消息:更新动态数据 - 性能优化:
// 优化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) } }) } - 错误恢复:
// 问题:一个渲染错误导致整个页面崩溃 // 解决方案: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 项目团队信息
团队成员:
- 申鹏
主要贡献: 核心算法、框架设计、问题排查 - 刘奕
主要贡献: 可视化界面、前端开发、文档整理
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: "加了任务队列和多阶段排序..."
分析过程:
- 复现bug (5min)
- 运行3个不同的traffic文件
- 都出现低完成率
- 根本原因分析 (5min)
- 打印算法调试信息
- 发现电梯卡在某个楼层无法决策
- 追踪发现 passenger_destinations 为空
- 代码review (10min)
- Pengshen: “乘客信息需要从哪里来?”
- LiuYi: “应该在on_passenger_call时记录”
- 一起检查系统是否自动填充
- 确认系统不填充,需要手动维护
- 修复 (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'"
排查过程:
- 开启debug模式 (5min)
- 在app.js中添加console.log
- 观察WebSocket消息是否到达
- 发现state.floors为undefined
- 原因分析 (5min)
- 旧逻辑:等待第一条state_update消息才初始化
- 问题:有时算法立即完成,没有state_update消息
- 解决方案:前端应该处理init消息,先初始化结构
- 设计修复方案 (10min)
- 后端GUIController.on_init()发送init消息
- 前端处理init消息,创建初始结构
- 前端处理state_update消息,更新动态数据
- 实现 (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 的优点
- 逻辑严谨 – 思维清晰,能迅速找到问题本质;例子:快速定位passenger_destinations问题
- 代码能力强 – 能快速将想法转化为代码;算法实现从0到可用很快
- 主动担责 – 问题出现时主动调试;”这个bug我来修”
- 对细节关注 – 代码review时能发现微妙的逻辑问题;提出的优化建议切实有效
Pengshen 的不足
- 有时陷入细节 – 例子:在某个bug上花了很久调试,后来发现改一个参数就解决;改进建议: 定期跳出细节,看看大局
YiLiu 的优点
- 沟通能力强 – 能用浅白的语言解释复杂的概念;很好地弥补了需求和实现之间的gap
- 全局视角 – 总能从用户角度思考功能设计;”这样设计用户容易理解吗?”
- 文档能力 – 文档清晰,注释详尽;chat文件夹的日志很专业
- 抗压能力强 – 在紧张的deadline面前保持冷静;能有效地安排优先级
YiLiu 的不足
- 对细节把握不够 – 有时focus在UI而忽视了后端数据流;例子:init消息设计时,一开始没想到数据初始化的问题;改进建议: 多想想数据的完整生命周期
9.5 如何说服伙伴改进缺点 – 三明治方法
三明治方法 (来自《构建之法》):
- 先说优点 (上层面包)
- 再说缺点 (中间的菜和肉)
- 最后说改进方向 (底层面包)
对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 文档的重要性
本项目的文档:
- CLAUDE.md – 编码规范
- README.md – 快速开始
- chat/*.md – 开发日志
- inline comments – 代码内注释
- 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的使用
应用场景:
- 代码生成 – 快速生成框架代码
- 问题诊断 – 分析复杂的bug
- 代码review – 提出优化建议
- 文档编写 – 生成初稿,然后修改
案例:
用户: "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怎么更漂亮?"
现在: 整体优化
"这个改动会不会影响其他模块?"
"测试覆盖全吗?"
"文档完善吗?"
总结
本次作业的主要成果
- ✅ 实现了高效的电梯调度算法 – 代码简洁(240行),易于维护
- ✅ 构建了完整的系统 – 后端:事件驱动架构,支持多种算法;前端:Web可视化,实时更新;通信:WebSocket,消息队列
- ✅ 深入应用了软件工程原则 – Information Hiding: 隐藏实现细节;Interface Design: 清晰的事件接口;Loose Coupling: 模块间独立
- ✅ 成功的结对编程实践 – 代码质量高,bug少;团队凝聚力强;知识转移充分
- ✅ 完善的文档和总结 – 开发日志详尽;代码注释清晰;最终报告完整
核心经验
- 简洁优于复杂 – 240行简洁代码优于1000行复杂代码
- 理解约束很重要 – 花时间理解LOOK算法的方向匹配约束
- 结对编程很高效 – 两个人的脑子确实优于一个
- 文档是知识的载体 – 好的日志和注释帮助学习
- 持续优化,永无止境 – 耗时还能做得更好
对后续团队的建议
- 建立统一的开发规范 (CLAUDE.md已完成)
- 定期阅读他人代码 – 学习不同的编程风格
- 重视测试 – 投入20%的时间做测试
- 使用AI工具,但不要依赖 – 工具是辅助
- 定期反思 – 像本报告一样总结经验
报告完成日期: 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条)
> AI辅助开发:我们使用 ChatGLM 与 Qwen3 协助生成部分框架代码,大幅节省时间,但仍需人工优化结构。
—
写短小的,明显没有 bug 的程序, 不要写很长,没有明显的 bug 的程序。