软件设计全流程 — 从需求到部署
一、需求分析
核心产出
需求分析不是”列出功能”,而是厘清约束 + 量化指标。
用户故事 ──→ 功能需求 ──→ 非功能需求(性能/安全/可用性)
│
└──→ 约束条件(平台/硬件/法规/工期)
文档模板
| 章节 | 内容 |
|---|---|
| 业务背景 | 谁用?解决什么问题?现有方案痛点? |
| 功能需求 | 用例图 + 优先级(MUST/SHOULD/COULD) |
| 非功能需求 | 性能指标(吞吐/延迟)、可靠性(MTBF)、安全等级 |
| 约束 | 操作系统、硬件接口、网络环境、合规要求 |
| 验收标准 | 每个需求的可验证条件 |
PulseQt 案例
| 要素 | 内容 |
|---|---|
| 业务 | 工业传感器数据采集,替代现有 LabVIEW 方案 |
| 功能 | TCP/串口双通道采集 → 实时曲线 → SQLite 存储 → 历史回放 |
| 非功能 | 100Hz 数据流不丢帧、24h 连续运行、<5ms 绘制延迟 |
| 约束 | Windows 10+、Qt 6.5、RS232 串口、Modbus 协议 |
常见陷阱
- 需求写的是方案而不是问题:
"用 Redis 缓存"→ 应写"查询延迟 <50ms" - 非功能需求缺失:“能跑就行” → 上线后 1000 条/秒就崩
- 验收标准模糊:“响应快” → 改为
"P99 延迟 <100ms"
二、系统架构 + 模块设计
需求分析和架构设计高度耦合,合并为系统设计阶段。
架构模式选型
| 模式 | 适用场景 | 不足 |
|---|---|---|
| 分层架构 | 企业应用、上位机 | 层间耦合可能导致级联修改 |
| 管道-过滤器 | 数据流处理、编译器 | 不适合交互式 UI |
| 微内核 | IDE、插件系统 | 性能开销 |
| 微服务 | 多团队、独立部署 | 网络延迟、运维复杂度 |
| CQRS/事件溯源 | 审计系统、高读写比 | 实现复杂度高 |
PulseQt 选型:分层架构
┌──────────────────────────────────┐
│ 表示层 (UI) │
│ MainWindow / RealTimeChart / │
│ QTableView / SettingsDialog │
├──────────────────────────────────┤
│ 业务逻辑层 │
│ ProtocolDecoder / DataBuffer / │
│ DatabaseManager │
├──────────────────────────────────┤
│ 通信适配层 │
│ TcpWorker / SerialWorker / │
│ ChannelManager │
├──────────────────────────────────┤
│ 硬件抽象层 │
│ QTcpSocket / QSerialPort / │
│ QSqlDatabase │
└──────────────────────────────────┘
层间通信:信号槽 (QueuedConnection)
依赖方向:上层依赖下层,下层不知上层
选型考量点
为什么选分层而不是管道-过滤器?
| 考量 | 分层 | 管道-过滤器 |
|---|---|---|
| 有 UI 交互(缩放、拖拽) | ✅ | ❌ 数据单向流动 |
| 需要历史数据回放 | ✅ 任意层可查询 | ❌ 需要重新流过管道 |
| 多通道(TCP/串口)切换 | ✅ 适配层抽象 | ❌ 需要不同的管道链 |
| 调试友好 | ✅ 层间信号可拦截 | ❌ 中间过滤节点难定位 |
为什么不是微服务? 单机桌面应用,不需要网络分布,微服务的服务发现/负载均衡/熔断全是过度设计。
模块划分原则
高内聚 + 低耦合 + 单一职责
✅ 好的拆分:
ProtocolDecoder — 只管帧解析,不管数据存哪、UI 怎么画
DatabaseManager — 只管读写 DB,不管数据从哪来
❌ 坏的拆分:
DataProcessor — 既解析协议、又写数据库、又更新 UI
三、通信协议 + 接口设计
协议和接口本质相同——都是组件间的契约,合并设计。
协议设计三要素
| 要素 | 说明 | PulseQt 实现 |
|---|---|---|
| 帧格式 | 如何定界、校验 | A5 5A 帧头 + 长度 + CRC16 |
| 交互模式 | 请求-响应/发布-订阅/双向 | 设备主动上报 + 上位机心跳请求 |
| 异常处理 | 超时、CRC 失败、断线重连 | 30s 心跳超时 → 自动重连 |
帧格式设计对比
| 方案 | 定界方式 | 优点 | 缺点 |
|---|---|---|---|
| 固定帧头+长度 | Header(2B) + Len(2B) | 高效、易实现 | 帧头可能出现在数据中 |
| 转义字符 | 0x7E 帧界 + 0x7D 转义 | 数据透明 | 带宽膨胀、实现复杂 |
| Modbus | 3.5 字符间隔 | 行业标准 | 仅 ASCII/RTU 模式 |
| JSON/文本行 | \n 分隔 | 人类可读、易调试 | 二进制数据需 Base64 |
PulseQt 选固定帧头+长度:工业场景二进制数据为主,效率优先。加 CRC16 校验,帧头冲突通过 A5 5A 双字节降低概率。
API 接口设计
上位机内部同样需要接口契约:
// 通信层对外接口(信号)
signals:
void rawDataReceived(const QByteArray &data); // 原始字节
void connectionStateChanged(bool connected);
// 数据层对外接口
class DataBuffer {
public:
void push(const DataPoint &dp); // 线程安全写入
QVector<DataPoint> snapshot() const; // 线程安全快照
signals:
void bufferUpdated(int count);
};
// 存储层对外接口
class DatabaseManager {
public:
void insert(const DataPoint &dp); // 缓冲写入
void flush(); // 强制落盘
QVector<DataPoint> queryRange(uint64_t from, uint64_t to);
};
接口设计原则
1. 契约优先:先定义接口签名,再实现
2. 最小接口:只暴露需要的,不暴露实现细节
3. 线程安全明确化:哪个线程调用?谁负责锁?
4. 错误返回明确:异常/optional/error_code,不用 -1 魔法值
5. 向后兼容:新增字段用默认值,不删旧字段
四、数据库设计
建模流程
实体识别 ──→ 属性定义 ──→ 关系建模 ──→ 索引优化 ──→ 读写分离策略
PulseQt 数据库设计
-- 采集数据表(写入密集型)
CREATE TABLE data_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL, -- 毫秒时间戳
channel INTEGER NOT NULL DEFAULT 0,
value REAL NOT NULL,
unit TEXT,
alarm INTEGER NOT NULL DEFAULT 0 -- 0=正常 1=警告 2=异常
);
-- 写入优化索引
CREATE INDEX idx_timestamp ON data_points(timestamp);
CREATE INDEX idx_channel_ts ON data_points(channel, timestamp);
-- 配置表(读多写少,键值对模式)
CREATE TABLE config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
-- 会话表(设备连接记录)
CREATE TABLE sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
start_time INTEGER NOT NULL,
end_time INTEGER,
device_id TEXT
);
存储引擎选型
| 考量 | SQLite (PulseQt 选择) | PostgreSQL |
|---|---|---|
| 部署复杂度 | 零 — 单文件 | 需要 service |
| 并发写入 | 单写者(WAL 缓解) | 多写者 |
| 数据量 | <1TB 胜任 | 无限 |
| 数据类型 | 弱类型 | 强类型 + JSON/数组 |
| 适用场景 | 嵌入式、桌面单机 | 服务端、多用户 |
写入优化策略
采集场景:100Hz × 3 通道 = 每秒 300 条 INSERT
❌ 每条 INSERT 单独提交 → 300 次 fsync/s → 极慢
✅ 批量提交:缓冲区满 100 条 → 事务提交 → 3 次 fsync/s
❌ 默认 journal 模式 → 读写互斥
✅ PRAGMA journal_mode=WAL → 读写并发
❌ 永远不清理 → DB 文件无限增长
✅ 定时 DELETE + VACUUM(或按时间分区)
五、测试设计
测试金字塔
┌──────┐
│ E2E │ 少:完整流程(串口→UI 显示)
┌┴──────┴┐
│ 集成 │ 中:模块间信号槽通路
┌┴────────┴┐
│ 单元 │ 多:ProtocolDecoder 状态机
└───────────┘
各层测试策略
| 层级 | 测什么 | 工具 | PulseQt 示例 |
|---|---|---|---|
| 单元 | 协议解码、CRC 计算、坐标变换 | GTest/QTest | timeToPixelX(ts) 边界值 |
| 集成 | 信号槽链路、DB 读写线程安全 | QTest | Worker→Parser→Buffer 链路 |
| E2E | 模拟器发帧 → 曲线显示 | 自研 mock | Python 模拟器 100Hz 24h 挂机 |
| 性能 | 100Hz 丢帧率、绘制帧率 | 自研 | 1h 连续采集帧计数 vs 期望 |
测试用例模板
用例 ID: TC-PROTO-001
标题: CRC 校验错误丢弃帧
前置: ProtocolDecoder 就绪
步骤:
1. 构造合法帧 → feed(validFrame)
2. 构造 CRC 错误帧 → feed(corruptFrame)
3. 构造合法帧 → feed(validFrame2)
预期:
frameDecoded 信号发射 2 次(错误帧被丢弃)
第 1 次数据 == validFrame 载荷
第 2 次数据 == validFrame2 载荷
测试数据管理
// 协议测试数据:用十六进制字面量定义
const QByteArray VALID_FRAME = QByteArray::fromHex(
"A55A000C01000203E8D4C2" // 帧头+长度+类型+4字节整数+CRC
);
// 大量数据点:用循环生成
auto testData = generateDataPoints(10000, // 数量
DataPoint{0, 0.0}, // 起始
DataPoint{10000, 100.0}); // 终止(线性插值)
六、部署与运维设计
部署不是最后才想的——在设计阶段就要决策。
决策清单
| 决策 | 选项 | PulseQt |
|---|---|---|
| 发行方式 | MSI/Portable/AppImage | Inno Setup |
| 自动更新 | Sparkle/自研 | 暂不需要 |
| 配置管理 | 注册表/ini/环境变量 | SQLite config 表 |
| 日志 | 文件/syslog/DB | 文件 + 分级 |
| 监控 | 健康检查端点/看门狗 | 心跳自检 |
| 备份 | 全量/增量/实时 | 定时备份 .db |
版本策略
v1.0.0
│ ├─ 主版本:不兼容的 API 变更
│ ├─ 次版本:向后兼容的新功能
│ └─ 修订版:向后兼容的 Bug 修复
Git 分支策略(单人项目):
master — 稳定版本,可随时发布
develop — 开发分支,功能完成后合入 master
feature/*— 大功能分支
日志设计
// 分级日志
enum LogLevel { DEBUG, INFO, WARN, ERROR, FATAL };
// 格式:时间 [级别] 模块: 消息
// 2025-01-25 14:30:00 [INFO] TcpWorker: Connected to 192.168.1.100:502
// 2025-01-25 14:30:05 [WARN] Heartbeat: Missed 2, retrying...
// 文件策略:按日期滚动,保留 30 天
log_2025-01-25.log
log_2025-01-24.log
...
部署检查清单
☐ Qt 运行时 DLL 已打包(windeployqt)
☐ SQLite/OpenSSL 等第三方库已包含
☐ Visual C++ 运行时已附带(或静态链接 /MT)
☐ 配置文件有默认值,首次运行自动生成
☐ 安装包有管理员权限声明(如需要)
☐ 安装路径无中文/空格(防止工具链问题)
☐ 崩溃时生成 dump 文件(Win: SetUnhandledExceptionFilter)
设计阶段总览
需求分析 ──→ 架构选型 ──→ 协议/接口定义 ──→ 数据库建模
│ │
│ ┌─────────────────────┘
│ ▼
│ 模块详细设计
│ │
│ ┌─────────┴─────────┐
│ ▼ ▼
│ 测试设计 部署设计
│ │ │
└────┴───────────────────┴──→ 验收交付
每个阶段的选择都在约束条件下做权衡:性能 vs 可维护性、简单 vs 灵活、开发速度 vs 代码质量。没有标准答案,只有适合当前项目的最优解。