TICKTICK
软件开发规范与架构设计
从项目目标、数据链路到模块接口,整理一套软件开发与架构设计方法。
- 一个开发者如果没有清晰的架构设计思路是非常危险的
- 程序开发的本质就是数据处理,所以你只要知道下一个环节需要输入什么从而得到什么就可以开始设计了
设计流程
- 定项目内容 确定项目要完成什么任务。 例:我要做一个双臂抓取系统,相机看到盒子 → 规划路径 → 机械臂抓走。
- 构思项目流程 根据任务来确定任务流程,有什么环节,输入输出是什么,画出大概的数据流向。 例:相机取图 → VLM 检测 → 深度投影 XYZ → 手眼变换 → 路径规划 → 机械臂运动。
- 定向技术栈 每个链路查资料来定性需要用到什么技术。列出技术栈。 例:深度相机用 Azure Kinect + pyk4a,检测用 LocateAnything-3B + YOLO, 控制用 TRON2 WebSocket,标定用 cv2.calibrateHandEye。
- 细化数据链路 对数据链路的每个节点进行模块化设计,定义每个模块的输入输出类型。 关键是:每个节点怎么死的也要想清楚。相机断流?模型 OOM?网络超时? 例:检测节点输入 BGR 图像 + text prompt → 输出 bbox 列表。 失败:超时则重试 3 次,仍失败则返回空列表并记日志。
- 规定模块化接口和设计总数据链路应用程序 确定总数据链路的执行程序,定义模块的输入输出签名(用 Protocol 而不是具体类), 从而完成整个数据链路的状态机设计和模块调用的方法。 核心规则:模块之间只依赖接口,不依赖具体实现。接口定义”这个模块能干什么”, 实现定义”用什么东西干”。app 负责把接口和具体实现接起来。
- 细化模块化架构 每个模块包自包含,拎到别的项目也能独立跑: ├── lib/ 二进制依赖(如 libk4a.so、.whl 等) ├── core/ 核心程序,只 import interface,不知道硬件是什么 ├── interface/ 对外暴露的 Protocol/ABC,定义输入输出契约 ├── config/ 本模块的默认配置 └── test/ 模块级别的测试(mock 替身放这里)
- 完成总应用程序设计(app 层) app 层只有一件事:组装。从各包拿 interface,创建具体实现,注入到状态机里。 自己构建状态机和使用范式,做到简洁多样。用 –args 增强范化性。 例:python app/grasp.py –mock → 全 mock 模式离线跑;不加 → 真设备。
- 设计应用脚本 应用脚本一键参数化启动。职责只有三件:source 环境 → 激活 conda → exec python app。
- 设计测试应用文件
分两层测:
- 包内 test/:测 core 逻辑,用 mock,不需要真设备
- 项目根 tests/:测 app 组装,端到端跑通
补充原则
- 依赖指向内层:app → interface(Protocol)← core,core 永远不知道 app 和具体硬件存在。
- 配置优先级链:环境变量 > CLI args > YAML 默认值。链断了要报错,不能静默降级。
- 可观测性:每个模块统一日志格式 [模块名] 状态,跑流水线时一目了然卡在哪一步。
项目构建范式
project/
├── docs/ # 项目文档
├── src/ # 模块包(每个自包含)
│ ├── package/
│ │ ├── lib/ # 外部二进制依赖:.so、.bin、.whl
│ │ ├── core/ # 核心程序(只依赖 interface,不依赖具体硬件)
│ │ ├── interface/ # Protocol 定义(抽象,不实现)
│ │ ├── config/ # 默认配置文件
│ │ └── test/ # 模块级测试
│ │ └── mock.py # 测试替身
├── app/ # 应用层(薄入口,只做组装)
│ └── task_app.py # 拿 interface → 创建实现 → 注入状态机
├── scripts/ # 启动脚本(配环境、可执行文件、启动程序脚本)
│ └── task.sh
├── tests/ # 端到端测试(测 app)
│ └── test_task.py
├── tools/ # 方便设定参数或者开发的外部工具
│ └── handeye_calibration # 例如相机标定工具
├── envs/ # 环境依赖文件
│ └── requirements.txt # 依赖清单
└── ReadME # 项目引导和总览
开发规范
没有开发规范,很容易把代码写的耦合和难读。有一个合理的开发规则可以让代码更加优雅高效可读和可移植。
应用开发-app
- 构建数据链路 根据项目任务规定数据流向和要使用到的模块
- 状态机设计 根据数据链路设定程序工作流程和阶段,形成稳定可用的闭环
模块开发-package
- 根据模块划分对象 根据模块任务合理构建对象。一个对象只负责模块里一个明确的职责。 判断标准:能用一句话说清楚”这个对象是干什么的”——说不清楚就该拆。
- 规划对象定位 规定对象的数据流位置和输入输出需求,为对象设定一个外部调用的接口。
- 给每个对象细分函数 设计对象内数据处理流程,把每个流程细分成多个对象的函数。
- 函数归属判断 这段代码放在对象里还是放外面?只有一个判断标准: 被多个对象复用的 → 写成全局函数,不绑在任何对象上 只属于这一个对象的 → 写成对象的方法 反例:一个四元数乘法函数,被 approach.py、executor.py、solver.py 都用, 你却写成了某个类的 @staticmethod。这就是放错地方了。
- 根据需求确定需要使用的库函数 能不自己写就不要自己写库函数,不要重复造轮子,除非它不好用或者太重。
错误处理设计
每种失败都要想清楚它属于哪一类,然后按类处理。不是每种错都 try/except 然后 pass。
分类框架:
- 可重试:网络超时、设备忙 → retry with backoff(等越来越久,最多 N 次)
- 可降级:主模型挂了 → 切换到备选方案(VLM 超时用 YOLO 顶、OSTrack 挂了复用框)
- 需恢复:状态丢失、连接断开 → 回到上一个安全状态(机械臂回 home、重置 session)
- 致命:硬件故障、超出限位 → 紧急停止,记录日志,通知操作者
每条数据链路上的模块,在设计阶段就要标注:失败模式 → 分类 → 处理策略。 不要等上线了才想”这个错误怎么办”。
Git 规范
Git 的核心不是”备份代码”,是”让别人(包括三个月后的你)能看懂这段代码是怎么演变过来的”。
分支策略: main 永远可部署。只接受 merge,不直接 commit feature/<功能名> 每个新功能从 main 拉分支,做完合回去 fix/<问题> 修 bug 从 main 拉,修完合回去 release/<版本号> 发布前冻结,只修 bug,不发版后合回 main 并打 tag
Commit 信息格式: <类型>: <简短描述>
类型:
改 功能变更或 bug 修复(最常用)
新 新增文件或模块
删 删除文件或废弃功能
重构 不改变行为的代码整理
文档 纯文档改动
例:
改 grasp/vlm: 修复手眼变换中 R_gripper2base 的求逆逻辑
新 camera: 新增 Kinect 深度 ROI 缩放功能
重构 lib/: 按模块分类整理目录结构
删 ros_ws: 删除 ROS2 工作空间,由 ros2_bridge 替代
规则:
- 第一行不超过 72 个字符(GitHub 折叠线)
- 说清楚"改了什么模块的什么内容,解决了什么问题"
- 别写"fix bug"、"update code"——等于没写
版本号(Semantic Versioning): MAJOR.MINOR.PATCH 例:1.3.2 MAJOR:不兼容的 API 改动(别人升级要改代码) MINOR:新增功能,向后兼容 PATCH:bug 修复,向后兼容 开发阶段用 0.Y.Z(0.1.0 → 0.2.0),API 稳定后升到 1.0.0
发布流程:
- 确认 main 分支所有测试通过
- 更新 pyproject.toml 的 version
- 写 CHANGELOG.md(改了啥、怎么升级)
- git tag vX.Y.Z
- git push origin main –tags
可观测性规范
程序跑起来之后你怎么知道它正常?不是盯着屏幕看日志,是有结构地暴露状态。
-
分层日志 [模块名] [级别] 消息 例:
[vision] infer frame 0: 1 box, 168ms [robot] TCP fetch timeout, retry 2/5 [grasp] skip target#2: Z=-0.72 < z_min=-0.67 -
关键节点打点 每个模块的入口、出口、失败点各打一行。一条流水线跑完,日志应该能拼出完整的时间线。
-
别淹没在日志里 INFO 是给人类看的(关键节点)。 DEBUG 是给排查用的(中间变量值)。默认跑 INFO,加 -v 切 DEBUG。
-
静默失败是 bug 任何被吞掉的异常必须写日志。try/except 里空 pass 是最危险的代码。
文档规范-docs
文档没人看只有两种可能:写太烂,或者找不到。大部分时候是找不到。
- 按用途分类,不是按时间 别用”2024-07笔记”这种命名。用”启动指南”、“架构设计”、“踩坑记录”。 新人进来第一件事是看启动指南,不是翻你的日记。
- 每篇文档只讲一件事 架构文档就别写安装步骤,故障手册就别画架构图。交叉引用用链接,别堆在一页。
- 先读后写,别编 读取目标代码和已有文档,基于事实生成。不确定的信息标注”待补充”,不要瞎写。
- 增量更新 已有文档只更新变化的部分,不重写整个文件。改动后列出更新了哪些文件。
目录结构:
docs/
├── design/<模块名>/ overview / data-flow / interface / pipeline / design-rationale
├── startup/ debug.md(调试启动)+ production.md(完整启动)
├── env-setup/ python.md + dependencies.md
├── dev-log/<日期>.md 今日完成、遇到的问题、明日计划
├── todo/ current.md(当前迭代)+ backlog.md(积压)
├── bugs/.md Bug 记录,含日期、严重程度、复现步骤、定位分析、修复记录
└── deprecated/<方案名>.md 废案:为什么提出、为什么废弃、教训
debug规范
不要”加个 print 看看哪错了”。用流程定位问题,五个步骤。
① 定位 — 找到根因,不是表象 读相关代码文件,分析可能原因,按可能性排序。 不确定的地方标注”需要确认”。不猜测,基于代码和事实。
② 讨论 — 和用户确认 复述你对问题的理解,确认无误再往下走。每次最多问 3 个问题。
③ 记录 — 写入文档 Bug 编号取 docs/bugs/ 下最大编号 +1(三位数),按模板写入。 内容:发现日期、严重程度、复现步骤、定位分析(引用代码行号)。
④ 方案 — 提供 2-3 个选择 每个方案标注改动范围、优缺点、风险等级。标明推荐哪个。 不给唯一选项,不替用户做决定。
⑤ 执行 — 等指令再改 重新读取文件确认最新内容,精确修改,输出修改摘要。 更新 Bug 文档的修复记录。
特殊情况:
- 用户说”直接修” → 跳过 ②③④,读取→修改→摘要
- 用户说”先记下来” → 只执行到 ③,不进入方案阶段
- 无法定位 → 诚实告知,建议提供更多日志或调试信息
代码规范
代码是写给人看的,顺便能在机器上跑。
- 命名说人话 get_image 比 proc_img 好。bbox_to_xyz_camera 比 b2x 好。 变量名不是越短越好,是越准确越好。
- 一个函数只做一件事 80 行的函数一般可以拆成 3 个。拆不出来就说明你没想清楚流程。
- 别写注释解释代码在干什么,写注释解释为什么这么干
循环 10 次 ← 废话,for i in range(10) 已经告诉你了
取 10 帧才检测是为了等相机曝光稳定 ← 这才值得写
- import 顺序和分组 标准库 → 第三方 → 本项目。中间空一行。 一眼能看出依赖了哪些外部包。
- 类型标注 输入输出标注类型不只是给 IDE 用,是给你自己用的。 三个月后回头看 def detect(image, text, mode, scale) 和 def detect(image: np.ndarray, text: str, mode: str = “phrase”) -> list[dict] 后者你能直接上手,前者你得再看一遍实现。
测试规范
根据代码类型选测试方式,不要强行套框架,不要为写测试而写测试。
- 纯逻辑模块 → 写测试脚本 没有硬件依赖的算法、解析器、数据变换,直接 assert。 每个测试函数独立运行,python tests/xxx.py 就能跑。
- 依赖硬件的模块 → 先 mock 能离线跑的绝不要插硬件。真机测试 5 分钟,mock 测试 0.5 秒。 开发阶段用 mock 快速迭代,确认逻辑通了对一次真机就够了。 不确定硬件行为时,先问”有模拟器吗?”
- 依赖外部服务的模块 → 集成测试脚本 数据库、网络 API、中间件,单独写脚本验证。
- 整机应用 → 进程监听 启动主进程,监听 stdout/stderr,验证进程存活和输出内容。 用 subprocess + assert,不要引入重型测试框架。
- 测边界,不测正常 空输入、深度全为 0、网络断连、bbox 在图像外面。 正常流程一般不会出错,踩坑的都是边界情况。
- 一个测试测一个行为 test_grasp_pipeline 不是好测试。test_pipeline_fails_without_camera 是。 看测试名字就知道测什么,挂了就知道哪里坏了。