可评审软件架构设计说明书:arc42、C4与Qt/智驾实践

可评审软件架构设计说明书:arc42、C4与Qt/智驾实践 简介这份《软件架构设计说明书》以图书杂志采购和借阅系统为对象面向项目经理、程序设计、测试及集成实施人员采用多视图方式完整表述系统架构与关键决策适合软件工程课程设计、架构文档撰写参考以及团队协作交付等场景。压缩包内仅1个docx文档约367KB篇幅紧凑却覆盖简介、架构表示方式、设计目标与约束、用例视图、逻辑视图、进程视图、实施视图、接口与通信、非功能性需求以及架构决策与评估等章节。目前已有1439人浏览学习。读者可据此理清从需求到构件的映射关系借鉴UML图表的组织方式与分层设计思路直接复用其文档结构与目录骨架用于撰写课程架构设计说明书或指导详细设计、集成测试与项目开发计划的制定。1. 软件架构设计说明书.docx 不是交付物清单而是可评审的架构契约评审会上被打回最多的架构文档通常不是页数不够而是答不出三个问题系统边界落在哪、关键质量属性靠什么机制兜底、一处改动会波及哪些模块。软件架构设计说明书.docx 这个文件名本身没有含金量含金量在于它是否是一份能逐条评审、能追溯到代码、能跟着版本走的契约。它要服务的读者有三类接手维护的人想知道模块职责与依赖方向测试的人想知道接口边界与异常路径评审的人想知道每个质量目标对应哪条设计决策。写给谁决定了写什么——如果一份说明书只描述了「系统分为表现层、业务层、数据层」那它只是目录不是架构。这一篇按「先在已有框架里把章节骨架立住再把 Qt 上位机与智驾这两类典型场景的条目写实最后让文档进入工程流程」的顺序展开适合正在写第一版架构说明书或者被评审打回需要重写的人。2. 用 arc42 与 C4 模型搭出软件架构设计说明书的主干章节不要从空白 Word 开始排版。行业里有两套互补的骨架可以直接借用arc42 负责「文档该有哪些章节」C4 模型负责「每个章节里的图该画到哪一层」。两者结合能把一份容易写成散文的说明书变成结构化、可逐条核对的清单。2.1 arc42 十二节与 C4 四层视图的对应关系arc42 给出固定的十二章结构好处是评审人不需要每次重新找内容在哪。C4 则约束视图的抽象层级Context系统与外部的关系、Container可独立部署的进程或服务、Component容器内部的模块、Code类级别通常不画全只在关键路径上补充。arc42 章节回答的问题对接的 C4 视图最小产出物1 引言与目标系统为什么存在Context 背景目标、干系人、质量目标2 约束哪些事不能改无技术栈约束、合规约束、团队约束3 上下文与范围边界在哪Context外部系统清单、交互协议4 解决方案策略大方向怎么定无3 到 5 条技术策略5 构建块视图拆成哪些模块Container Component构建块表、依赖方向6 运行时视图跑起来怎么交互Container关键场景时序、线程模型7 部署视图装在哪、怎么发Container节点拓扑、部署单元8 横切概念全局一致的约定无日志、错误码、配置、鉴权9 架构决策为什么这么选无ADR 列表含状态与替代方案10 质量需求达标线是多少无质量属性场景表11 风险与技术债哪里会出问题无风险条目、缓解措施12 术语表名词怎么统一无术语与缩写我一般会保留这张表作为说明书的第一页附录让评审人知道后面十二节分别对应什么。章节 5、6、7 是重灾区也是评审最集中火力的地方后面两章会重点展开。章节 10 常见的写法错误是只写「系统应具备高可用性」而可评审的写法必须包含刺激、环境、响应、度量四个部分例如「当采集线程连续 3 次读取超时刺激在正常运行环境环境下系统应切换到备用通道并记录告警响应切换耗时不超过 500ms度量」。2.2 用 YAML 做单一数据源脚本生成 .docx 骨架直接在 Word 里维护十二章最常见的后果是编号错乱、表格样式不一致、改一处要翻十页。更稳的做法是用一份 YAML 描述结构用脚本生成带标题层级和表格的 .docxWord 只作为最终排版与评审载体。# arch/architecture.yaml meta: system: XH-2000 数据采集上位机 doc_version: 1.4.0 owner: 架构组 sections: - id: 01 title: 引言与目标 required: [目标, 干系人, 质量目标] body: 本系统面向产线设备的数据采集与可视化目标是…… - id: 05 title: 构建块视图 required: [构建块表, 依赖方向, 接口清单] body: 系统划分为 UI 层、应用服务层、设备抽象层、基础设施层……# tools/build_doc.py from docx import Document from docx.shared import Pt import yaml, sys def build(spec_path: str, out_path: str) - None: spec yaml.safe_load(open(spec_path, encodingutf-8)) doc Document() # 文档标题用大字号不进目录编号体系 h doc.add_heading(spec[meta][system], level0) doc.add_paragraph(f文档版本{spec[meta][doc_version]} 负责人{spec[meta][owner]}) for sec in spec[sections]: # arc42 的章节号写进标题文本保证 Word 导航窗格可检索 doc.add_heading(f{sec[id]} {sec[title]}, level1) doc.add_paragraph(sec.get(body, 待补充)) # required 字段落成表格评审人按行打勾避免漏项 if sec.get(required): table doc.add_table(rows1, cols3) table.style Table Grid hdr table.rows[0].cells hdr[0].text, hdr[1].text, hdr[2].text 条目, 内容, 状态 for name in sec[required]: row table.add_row().cells row[0].text, row[1].text, row[2].text name, , 待评审 doc.save(out_path) if __name__ __main__: build(sys.argv[1], sys.argv[2])运行方式是两条参数第一个参数指向 YAML 源第二个参数是输出的文件名。section.id用两位字符串而不是整数是为了让 Word 目录里的「01、02、10」保持字典序与视觉序一致避免出现 1、10、2 这种排序。required字段是这套方法的核心——它把「这一章必须回答什么」从口头约定变成生成出来的空表格评审前谁的表格还空着一眼就能看出来。表格样式固定为Table Grid是为了让评审批注和修订痕迹落在单元格里而不是在浮动文本框里丢失。提示docx 是二进制格式不要把它当作唯一源文件。YAML 进 Gitdocx 只作为构建产物这样每次评审看到的是同一套结构而不是某个人手工调整过的版本。2.3 章节最小完整度的自动检查骨架生成之后还要防止「表格生成出来但没人填」。写一个轻量校验脚本在提交前跑一遍把缺失项挡在评审之前。# tools/check_doc.py import yaml, sys # 每章的关键字命中即认为该章有实质内容 KEYWORDS { 01: [目标, 干系人], 03: [外部系统, 接口], 05: [构建块, 依赖], 09: [决策, 替代方案], 10: [刺激, 度量], } spec yaml.safe_load(open(sys.argv[1], encodingutf-8)) missing [] for sec in spec[sections]: kws KEYWORDS.get(sec[id]) if not kws: continue text sec.get(body, ) for kw in kws: if kw not in text: missing.append(f{sec[id]} {sec[title]} 缺少「{kw}」) if missing: print(\n.join(missing)) sys.exit(1) # 非零退出码便于挂在 CI 的 pre-commit 阶段 print(章节完整度检查通过)这套检查不判断内容写得好不好只判断「该说的词有没有出现」属于低成本高收益的守门员。第 9 章的关键是「替代方案」——一份只写「我们选了消息队列」而不写「为什么不选共享内存和直接 RPC」的决策记录半年后没人敢改。第 10 章的关键是「刺激」与「度量」缺了这两个词质量需求就退化成口号。把sys.exit(1)留在脚本里才能让它在自动化流程中真正拦住人否则输出一行提示很容易被刷过去。3. Qt 上位机软件架构在说明书里的分层与线程约定Qt 上位机的架构文档有个特殊难点它既要描述模块分层又要描述线程归属。分层写错了还能改线程归属写错了就是随机崩溃和偶发卡顿而且极难复现。所以这一章的说明书必须把「哪段代码跑在哪个线程」写成硬约束。3.1 四层划分与线程所有权表常见的可靠划分是四层依赖方向单向向下禁止下层反向调用上层。层职责允许依赖运行线程代表类UI 层展示与用户输入应用服务层GUI 主线程MainWindow、各 View应用服务层用例编排、状态机设备抽象层、基础设施层GUI 主线程或独立工作线程AcquisitionService设备抽象层屏蔽协议差异基础设施层各自工作线程DeviceWorker 子类基础设施层串口、网络、日志、配置无随调用方线程QSerialPort 封装、Logger这张表要写进说明书第 5 章同时在第 6 章运行时视图里补一张线程所有权表逐条列出「对象、创建线程、销毁线程、跨线程访问方式」。QObject 的父子关系不能跨线程QSerialPort必须在它实际使用的线程里创建否则会出现「句柄有效但读不到数据」这类不报错的故障。跨线程通信统一走信号槽连接类型在说明书里写死为Qt::QueuedConnection不要依赖自动连接因为自动连接的判断依据是接收者线程一旦后续把对象移到别的线程行为就变了。3.2 通信与设备抽象层的接口约定设备抽象层要写成接口清单而不是描述性的文字。说明书里至少要覆盖协议类型、连接参数、超时阈值、重连策略、数据缓冲上限五列。接口协议关键参数超时重连策略缓冲上限DeviceA串口115200 8N1读 200ms3 次退避 1s/2s/4s4096 帧DeviceBModbus TCP单元号 1端口 502请求 500ms断链后 2s 轮询重连1024 帧DeviceCOPC UA安全策略 None会话 60s订阅 1000ms会话失效重建2048 帧缓冲上限这一列最容易被漏掉它是防止 UI 卡死的最后一道闸门采集速率高于消费速率时如果没有上限策略内存会持续增长。说明书要明确规定超限后的行为——丢弃最旧数据并计数告警还是阻塞生产者两种选择对实时性的影响完全不同必须在文档里写明而不是留给实现者临场决定。3.3 用一段 Qt 代码验证说明书里的线程约定文档写完之后用一段最小代码验证约定是否可落地比在评审会上口头解释有效得多。// deviceworker.h class DeviceWorker : public QObject { Q_OBJECT public: explicit DeviceWorker(QObject *parent nullptr); public slots: void start(); // 在工作线程中打开设备禁止在构造函数里打开 void stop(); // 优雅关闭等待写缓冲清空 signals: void frameReady(const QByteArray frame); // 跨线程投递到 UI 层 void errorOccurred(int code, const QString msg); private: QSerialPort *port_ nullptr; // 生命周期由本线程控制 }; // main.cpp 片段 auto *thread new QThread; auto *worker new DeviceWorker; // 不指定 parent稍后 moveToThread worker-moveToThread(thread); // 用 QueuedConnection 明确跨线程语义不依赖自动连接 QObject::connect(thread, QThread::started, worker, DeviceWorker::start); QObject::connect(worker, DeviceWorker::frameReady, view, MainView::appendFrame, Qt::QueuedConnection); thread-start();DeviceWorker不指定父对象是因为带父对象的 QObject 无法被moveToThread移走。start()放在槽函数里而不是构造函数里是为了保证QSerialPort的创建发生在工作线程内——构造函数在moveToThread之前执行那时对象仍属于创建它的线程。信号槽显式指定QueuedConnection意味着frameReady里的QByteArray会被拷贝进事件队列帧率很高时这份拷贝会成为瓶颈说明书应在此处补一条约定高频路径改用共享缓冲加序列号而不是每帧都走信号槽。4. 智驾软件架构说明书的实时性预算与安全条目写法智驾这类场景的架构说明书跟普通业务系统的最大差别是它必须把「时间」和「失效」写成可核对的数字。描述性文字在这里几乎没有评审价值评审人要的是预算表、触发条件和验证方法。4.1 端到端时延预算表与抖动指标时延必须拆到环节并且每一段都要指明在哪测量。只写「感知到执行不超过 100ms」是没法验证的。环节预算测量点周期抖动上限传感器采集10ms驱动时间戳10ms±1ms融合处理30ms节点入口到出口20ms±3ms决策规划40ms输入帧到轨迹输出20ms±5ms控制下发15ms轨迹到总线报文10ms±1ms合计95ms首尾时间戳差——预算表的合计值要留出余量不能刚好等于指标上限因为抖动叠加会吃掉余量。抖动上限这一列是区分「平均能跑」和「最坏情况能跑」的关键评审时优先看它。表里每一行的测量点必须是可以在日志或总线上抓到的实际时间戳如果某个环节找不到测量点说明该模块的可观测性设计缺失应该回到第 8 章横切概念里补埋点约定。4.2 安全机制与降级策略的条目化写法安全相关的内容不要写成段落写成字段固定的条目每条包含六项条目 ID、触发条件、检测机制、响应时间、降级后能力、验证方法。以「感知输入超时」为例条目 ID 为 SAF-012触发条件是连续两个周期未收到融合结果检测机制是看门狗计数器溢出响应时间是 50ms 内切换降级后能力是限速并保持车道验证方法是注入延迟故障并抓取切换时间戳。这样写的好处是每一条都能直接对应一个测试用例追溯矩阵里也能一行行勾选。功能安全等级分配例如某功能分解为 ASIL B、某功能为 ASIL D要在第 2 章约束里写清楚并说明分解依据来自危害分析与安全目标而不是拍脑袋决定。降级策略必须写明降级后的能力边界只写「进入安全状态」是不够的——是停车、限速还是退出功能直接决定了测试怎么设计。4.3 用脚本校验时延预算与追溯矩阵预算表放在文档里会随时间失真把它放进 YAML 并做一次求和校验能提前发现超预算的改动。# tools/check_latency.py import yaml, sys LIMIT_MS 100.0 # 端到端指标上限 MARGIN_RATIO 0.10 # 至少保留 10% 余量 data yaml.safe_load(open(sys.argv[1], encodingutf-8)) total sum(stage[budget] for stage in data[stages]) worst sum(stage[budget] stage[jitter] for stage in data[stages]) print(f标称合计 {total:.1f}ms最坏合计 {worst:.1f}ms上限 {LIMIT_MS}ms) if total LIMIT_MS * (1 - MARGIN_RATIO): sys.exit(标称值余量不足请重新分配各环节预算) if worst LIMIT_MS: sys.exit(最坏情况超出指标需要压缩抖动或削减预算)脚本判断两次先看标称值是否留够余量再看「预算加抖动」的最坏值是否越界。第二项才是真正卡人的地方——很多方案标称值 95ms 看着很安全抖动一叠加就冲到 105ms。把这两个阈值写成常量而不是散在文档文字里改动预算时脚本会立刻报警。追溯矩阵可以用同样的思路用一张 CSV 维护「安全目标 ID、组件、接口、测试用例 ID」四列缺任何一列就打回避免出现「安全目标写了但没有任何测试覆盖」的悬空条目。5. 让软件架构设计说明书.docx 进入版本控制与持续检查5.1 用 pandoc 把 docx 转成可 diff 的文本docx 二进制没法做有意义的 diff评审时常见的场景是「这版和上版差了什么」只能靠人工比对。可行的做法是保留 Markdown 作为源docx 作为发布产物向外发的是排版好的 .docx内部流转的是文本。# 用参考模板生成 docx保证字体、页边距、标题样式与团队模板一致 pandoc arch.md -o 软件架构设计说明书.docx --reference-doctemplate.docx # 反向转换用于对比历史版本 pandoc 软件架构设计说明书.docx -t gfm -o arch_current.md git diff --stat arch_prev.md arch_current.md--reference-doc是关键参数它让生成出来的文档直接套用既有模板的样式而不是 pandoc 默认样式省掉每版手工调格式的时间。反向转换主要用来做存档对比把上一版 docx 转成 Markdown 存进仓库下一次改动就能用git diff看出具体增删了哪几条决策、哪个参数被改了。这套流程的代价是多维护一份 Markdown收益是评审意见可以按行提而不是「第 37 页那段」这种定位。5.2 三个值得挂在提交钩子上的检查除了前面的章节完整度和时延预算检查还可以加三项成本很低但很有效的校验。第一项是术语一致性从第 12 章术语表读出所有术语扫描全文出现同义异名例如「采集卡」和「采集板卡」混用就报警这类不一致在跨团队评审时最容易引发误解。第二项是 ADR 状态检查每条架构决策必须有状态字段取值限定为提议、已接受、已废弃、被替代缺少状态或被替代但没写替代者的一律打回。第三项是编号连续性arc42 章节号、构建块编号、安全条目编号都不允许出现断号断号通常意味着有人删了内容却没更新引用。这三项都可以写成对 YAML 或 Markdown 的纯文本检查几十行代码就够挂在pre-commit上让文档质量和代码质量走同一套门禁。真正的收益不在于拦住了多少错误而在于把「说明书该写成什么样」从口头约定变成了可执行规则——新加入的人跑一次检查就知道这份文档的及格线在哪。本文还有配套的精品资源点击获取