上位机开发日记 · 第 2 篇 · 架构先行:六层分层与边界

阅读时长 :约 5 分钟 · 难度 :入门 · 前置知识 :第 1 篇(定位与三问选型)
本篇回答一个问题:代码该怎么分家。
1. 一个两个月后才会发作的病
项目启动时,你想着"就几个按钮,一个文件够了":
python
# main_window.py ------ 两周后 3000 行
class MainWindow(QMainWindow):
def on_start_clicked(self):
self.ser = serial.Serial("COM3", 115200) # 开串口
frame = b"\xAA\x55" + cmd + crc # 拼帧
self.ser.write(frame) # 发指令
data = self.ser.read(1024) # 读数据
values = struct.unpack("<10h", data[4:24]) # 解包
for i, v in enumerate(values): # 洗数据
self.table.setItem(0, i, QTableWidgetItem(str(v * 0.5)))
self.curve.setData(ts, values) # 画图
open("log.csv", "a").write(",".join(...)) # 存盘
一个月后,需求来了三个:换成网口、加个 CRC 校验、数据要存成二进制。你打开这个文件,发现任何一处修改都可能踩到别的功能------改通讯要动界面代码,改界面要重看协议细节。
这不是代码量的问题,是没有分层:一次点击,从物理层干到了表现层。
2. 六层结构
上位机的标准分工是这样的:

【判断】中间三层(协议、设备、数据)是"看不见的承重墙"。新手容易把注意力全花在最上面那层,但这三层才是决定项目能不能活到第二年的关键。
3. 每一层该做什么、不该做什么
| 层 | 该做什么 | 不该做什么 |
|---|---|---|
| UI 表现层 | 控件、事件、绘制、坐标轴、状态显示 | 不组帧、不碰串口、不做业务计算 |
| 应用 / 业务层 | 流程编排、状态迁移、指令序列、合格判定 | 不 import 任何 GUI 库 |
| 协议 / 通讯层 | 组帧解帧、校验、超时重传、心跳 | 不关心数据含义,不知道界面上是什么 |
| 设备抽象层 | 统一 open / close / read / write 接口 |
不实现协议细节,不做业务判断 |
| 数据层 | 缓存、格式、落盘、回放、时间戳 | 不判断"这个值合格吗"(那是业务层的事) |
| 基础设施 | 日志、配置、异常体系、打包脚本 | 不掺业务逻辑 |
关键理解:"不该做什么"比"该做什么"更重要
以协议层为例。它解出一帧数据后,只知道"第 3 到第 5 字节是一个 16 位小端整数,物理量纲是 mV",它不该知道这个值代表"驻波比"还是"增益",更不该知道"超过 2.0 就要变红"。
因为一旦协议层知道了业务含义,换一个仪器型号时,你就要同时改协议层和业务层------耦合就是这样长出来的。
同理,设备抽象层的存在意义是:上层代码写的是 dev.read(),不管是串口、TCP 还是 VISA 后端,上层一行都不用改。 这是第 4、5 篇要重点打的底子。
4. 一条硬检验标准
分层做得好不好,不需要凭感觉。用这一条检验:
把 UI 层整个删掉,其他层能否照常跑通并跑出数据?
- 能 → 分层是对的。业务逻辑在正确的位置,界面只是"显示器"。
- 不能 → 说明业务逻辑漏进了界面层。这是最难还的债,趁早拆。

【判断】实操建议:写一个 main_cli.py,不用任何图形界面,纯命令行跑通"连接 → 采集 → 存盘"全流程。它既是分层检验器,也是第 1 篇那个"设备模拟器"的雏形。
5. 三种常见的分层反模式
| 反模式 | 症状 | 后果 |
|---|---|---|
| 上帝窗口 | 所有逻辑写在 MainWindow 的方法里 |
无法单测,无法复用,无法换界面 |
| 深入调用 | UI 层直接调 serial.write() |
换通讯介质要改界面代码 |
| 数据回灌 | 数据层反调界面更新显示 | 循环依赖,界面卡死时整条链断掉 |
【判断】第三种最隐蔽。正确方向永远是单向的:数据从下往上流,指令从上往下流,绝不允许下层回头调用上层。
6. 分层带来的实际收益
| 收益 | 具体表现 |
|---|---|
| 可测试 | 协议层、业务层可以脱离硬件跑单元测试(见第 12 篇) |
| 可替换 | 换仪器、换介质、换界面框架,只动一层 |
| 可并行 | 硬件没到位时,用模拟器先把上层写完 |
| 可维护 | 定位问题只需问"这是哪一层的事" |
第 5 篇那套解帧器代码,之所以能直接拿来用,就是因为它是纯函数式的协议层代码------喂字节进去,吐帧出来,不依赖任何界面和设备。
7. 本篇小结
| 记住这一条 | 说明 |
|---|---|
| 六层各司其职 | UI、业务、协议、设备、数据、基础设施 |
| 边界靠"不该做什么"划 | 协议层不知道业务含义,数据层不做合格判定 |
| 分层硬标准 | 删掉 UI 还能跑通,就是对的 |
| 数据流单向 | 数据向上流,指令向下流,绝不回头 |
| 承重墙在中间三层 | 协议、设备、数据决定了项目能活多久 |
8. 动手练习
给你的项目画一张分层草图,然后回答:
- 你现在写的代码,分别落在哪一层?(如果有代码落不进任何一层,说明边界还没划清)
- 你现在有没有"UI 层直接开串口"的地方?有的话标记出来,那是第一批要重构的目标。
- 如果明天要把串口换成网口,你需要改几个文件?理想答案是一个都不改。
上一篇 :第 1 篇 · 上位机到底在解决什么问题
下一篇 :第 3 篇 · 开发流程:六个阶段与验收标准
------从"分层"到"分步":先做什么、后做什么,每步怎么算做完。