2026-09-22 · 每日技术导师 · 接续 09-19《Qt Model/View 架构深潜》与 09-04《插件化架构》,从「技术深度」转向「让技术深度在团队中复利」
过去十几篇我们一直在往深处挖:C++ 对象模型、Qt 元对象系统、事件循环、渲染管线、CMake 工程体系。这些能力解决的是「我一个人能不能做对」;但从 Team Leader 走向桌面客户端架构师,真正的瓶颈会换一个位置——变成「一个十人团队,在两年时间里,能不能持续做对」。技术决策的不可逆程度决定了它比任何一次代码提交都更贵:一次提交写错了,revert 一分钟;把整个客户端的技术栈押在某个渲染方案上,错了要还两年。
所以今天要学的不是某个 API,而是一套把技术判断变成组织能力的基础设施:用 ADR 记录决策、用单向门/双向门给决策分级、用三层评审保证质量下限、用康威定律审视组织结构、用技能矩阵把个人能力变成团队能力。这套东西在 Web 前端团队里你其实已经零散见过(RFC、PR Review、lint 规则),只是当时没有被你显式地建成体系。
贝索斯提出的 one-way door / two-way door 是团队管理里性价比最高的一条判据。双向门决策可以低成本回退(日志库换个实现、某个类的接口调整),过重流程只会拖慢团队;单向门决策回退代价极高(Qt 版本基线的选定、Widgets 还是 QML 作为主 UI 技术、进程模型是单进程还是多进程 + IPC、是否引入商业授权依赖),必须上会、必须记录、必须留下复审触发器。管理者的核心动作是把决策预先分类,而不是把所有事情都用一套流程处理——把双向门也送进评审会,是团队节奏被拖死的第一原因。
Michael Nygard 在 2011 年提出的 Architecture Decision Record,本质是把「决策」当成一个版本化制品。一条 ADR 只需四件事:Context(当时的约束与事实,例如「Qt 5.15 已进入商业支持末期、我们要跨平台且必须用 CMake」)、Decision(明确的一句话决定,用主动语态:「我们采用 Qt6 + CMake target 体系」)、Status(proposed / accepted / superseded / deprecated)、Consequences(正反面后果都写,包括「旧插件 ABI 需要重新编译」这种疼的地方)。再补两块工程化字段:Alternatives considered(考虑过什么、为什么没选,这是半年后别人不推翻你的唯一保险)和 Revisit trigger(什么条件出现时应当重新评估,例如「Qt6 某模块被弃用」「团队 QML 人力达到 3 人」)。ADR 放在仓库 docs/adr/0007-qt6-migration.md 里跟代码同生命周期——文档不入仓,等于没写。
把三层混用是常见失控点:在 PR 里吵架构,等于让最重要的问题在最晚、信息最少的时间点被讨论。
康威定律:系统架构必然映射组织的沟通结构。你有「UI 组」和「内核组」,最后就得到一个沟通成本极高的分层接口;你想让插件体系成功,就必须先有能独立拥有插件的团队。Team Topologies 把团队分为 stream-aligned(对齐业务流的交付团队)、platform(提供内部平台,如构建/发布/UI 组件库)、enabling(短期赋能其他团队,如 Qt 性能小组)、complicated-subsystem(专攻难题,如渲染器)。对客户端团队可直接套用:架构师常常应当是 platform 或 enabling 角色,而不是「什么都自己写的那个人」。架构调整要问一句:这次改动是不是需要同步调整团队的交互模式(长期配对、内源贡献)?
共识驱动在技术决策上常常是最慢又最模糊的路径——每个人都有否决权,于是没人有责任。实践里常用 DACI/RACI 明确四种角色:Driver(推进者)、Approver(唯一决策者)、Contributor(提供输入)、Informed(被告知)。架构师的成熟标志是:愿意在信息不完整时给出可撤销的决定,并明确谁是 Approver。技术评审不是投票,而是「决策者听完反对意见后负责」。
把团队能力当成可观测系统:一张技能矩阵(行是人,列是能力项如 Modern C++ 语义、Qt Model/View、并发与线程亲和性、CMake 与打包、调试与性能剖析),每格标 0–3 级。矩阵立刻暴露三类风险:单点(某项只有一人 3 级)、断层(没人 2 级以上)、错配(人成长目标与项目任务长期不一致)。配套动作是评审轮值和结对:让 1 级的人评审 2 级人的代码、3 级的人只做兜底,是性价比最高的培养方式。
这是典型单向门。约束事实包括:Qt 5.15 的商业支持窗口收尾、Qt6 全面转向 CMake(qmake 不再是主力)、QRegExp/QTextCodec/QString::split 默认行为等 API 变化、Qt6 的 QML 编译(qmlcachegen、qt_add_qml_module)显著改善启动性能。这些事实不改,迁移理由会随时间自我强化——所以正确做法是把事实与决定分开写进 ADR,而不是在群里说「Qt6 是新版本,该升了」。反面案例很常见:团队没有记录备选方案(例如分模块灰度迁移、先迁构建体系再迁 API),两年后新人会重新提出一遍已经被否决过的方案。
判据往往不是「哪个更好」,而是你的团队和产品节奏长什么样。VSCode 的界面高度自定义、需要密集文本与命令流,选择了自绘(Canvas/DOM 与自定义渲染)+ 命令式编程,性能与可控性优先,风格由主题系统而非回退到框架动效;而不少国内 IM 类客户端(QQ、微信等)在皮肤、动效、多端一致性上投入大,向声明式 UI 靠拢收益更高。结论不是某一个选项对,而是:决策必须写明「我们的团队现在拥有什么能力、产品未来两年最需要什么」。如果团队只有 0.5 个人懂 QML,却因为「QML 更现代」把主界面押过去,那是一次以组织能力赌技术趋势的错误下注。
Google 的工程实践文档里有一条反直觉的规则:评审者应当默认批准,除非能证明这次提交使代码质量下降。它设计出来是为了对抗两种失败模式——评审者把自己变成守门人,导致评审成为团队瓶颈;以及评审沦为风格战场。同理,JetBrains 这类公司把「设计评审」作为正式环节:先看接口与责任划分,再看实现。改变代码质量下限的从来不是天才,而是评审标准被写下来、被自动化、被持续执行。
如果你有两个仓:client-ui 与 client-core,由两个团队分别拥有,那么最终接口一定会变成「又厚又稳定、谁都不敢改」的窄腰,跨仓改动要两轮排期。插件化架构(09-04 那篇)能不能落地,先看团队边界是否允许插件作者独立发布。当架构卡住时,先看组织结构;当组织优化时,别忘了架构也需要同步调整。平台组(platform team)存在的意义正是把这种跨团队的公共能力(构建、发布、UI 组件、日志与崩溃上报)从每个交付团队身上剥出来。
错误 1:把所有决策都当单向门,流程压死节奏。
原因:管理者把「严谨」等同于「全流程评审」,或用流程来转移决策责任。后果是团队开始绕开流程(在私聊里定事)。
如何避免:在团队章程里明确三类决策的路径——可逆(作者自主 + 事后通知)、中等(一位 Approver + 24 小时异议窗口)、不可逆(ADR + 设计评审 + 复审触发器)。
错误 2:ADR 写成会议纪要。
原因:只记录「决定了什么」,不记录当时的事实约束与备选方案,也没有 Consequences。
如何避免:把 Context 写成可验证的事实(版本、期限、人力、性能数字),并强制写「我们否决了什么、为什么」;用模板 PR 检查必备字段(本篇文章的实践任务就是把它做成一个可执行的门禁)。
错误 3:用人肉评审风格,把 Review 变成消耗战。
原因:缺少格式化与静态检查的自动化基线,于是每个人都在用自己的审美标准发言;同时对同一处代码反复来回复。
如何避免:clang-format(含 .clang-format)+ clang-tidy(含 .clang-tidy)+ CTest 作为合入门禁;评审意见分级标记(blocking / suggestion / nit),nit 不可阻塞合入。
错误 4:评审没有时限,成为交付瓶颈。
原因:评审者把自己当唯一守门人,且没有排班机制;PR 越挂越大。
如何避免:约定 SLA(例如工作日 4 小时内首次响应)+ 评审轮值;PR 控制在可读规模(经验值 400 行以内);紧急通道与事后补审要显式写出来,别靠默契。
错误 5:只做代码评审,不做设计评审,也不做复盘。
原因:代码评审看得见、好量化、有仪式感;而设计评审需要提前对齐,复盘要面对失败。
如何避免:把设计评审放进需求流程的必经节点(无 ADR 不开工,除非属于可逆类);复盘只看流程与系统,禁止归因到人(blameless),并强制产出带负责人与截止日的改进项。
superseded by 0012,而不是编辑历史。好处是新人能从时间线看到「为什么当年那么选」。何时不用:一次性的技术尝试(spike)不要写 ADR,用实验记录即可;真正的单向门才值得。作为前端出身的架构师,你手里有一副现成的牌,只是没被当成制度用起来。
RFC 流程 ↔ ADR。Vue 3 的每个重大特性都先走 RFC(Request for Comments):写清动机、设计、备选方案、缺点,公开讨论后才实现;这正是 ADR 的「决策前制品」形态。你在前端团队里可能只是「拉个群讨论」,到了 C++ 世界,把讨论固化成 Markdown 入仓就是同一件事的更严版本。
ESLint / Prettier / lint-staged ↔ clang-format / clang-tidy / pre-commit。前端早就承认「风格问题不该由人评审」,所以才有格式化工具与 CI 卡口。C++ 的等价物是 .clang-format + .clang-tidy + CTest 门禁;你在前端建立的「机器管风格、人管设计」原则可直接平移,只是 C++ 里机器能救你的地方更多——未初始化成员、悬垂引用、隐式拷贝这类问题,静态分析和 -Wall -Wextra -Werror 都能在合入前拦住。
平台组 ↔ 前端组件库 / 构建基建团队。前端的 monorepo + 组件库 + CI 模板团队,就是 Team Topologies 里的 platform team;在客户端项目里对应「构建与发布、UI 组件库、日志与崩溃上报」的公共能力团队。康威定律在前端同样成立:微前端拆分的边界,最终就是组织结构与团队自治边界的映射——插件化架构(09-04)和微前端是同一个组织问题的两种技术表达。
版本与依赖矩阵 ↔ 运行时/框架兼容矩阵。前端有 Node LTS 策略、浏览器兼容矩阵、canary 版本;客户端有 Qt LTS 与商业支持窗口、编译器(MSVC/GCC/Clang)与 C++ 标准版本矩阵、vcpkg 依赖治理。它们的决策结构完全相同:支持多久、谁负责升级、什么时候必须升级——所以这些天然就是 ADR 题目。
理解层面:你的产出不再是代码行数,而是「团队决策的平均质量」与「决策的返工成本」。首先要抵抗的冲动是替所有人做决定——那会让团队失去判断力,也让你成为瓶颈;其次才是建立流程。判断自己做得对不对,看两件事:团队里有多少「不需要问你也能做对」的决策,以及不可逆决策的返工率。
指导层面:用问题代替答案(「这个接口下一年谁改它?」「如果 QML 人力只剩一人,这个方案还成立吗?」),让成员自己写出 Consequences。给成长路径配任务:想升到架构的成员,第一课不是写更难的代码,而是独立产出一份能被评审通过的 ADR,并负责它在三个月后的复审。
Code Review 关注点(C++/Qt 专用顺序):①生命周期与资源语义——RAII 是否完整、有无悬垂引用/裸 new、对象是否在正确线程(QObject 亲和性与信号槽连接类型);②接口契约——const 正确性、错误处理策略是否统一(异常 vs std::expected)、拷贝与移动语义是否明确;③可维护性——职责是否单一、是否有测试;④风格——交给工具。评审意见要说明「为什么」(尤其涉及未定义行为与性能时),对新人用提问式评论,对重复出现的同类问题,转化为规则或静态检查项,而不是第 N 次口头提醒。
1. 你团队过去半年做过的决策里,有哪些其实是单向门却被当成双向门处理(没有记录、没有评审)?如果今天要补一份 ADR,Consequences 里最难写的一条是什么?
2. 如果要为「Qt Widgets 还是 QML 作为主 UI 技术」写一份 ADR,你的 Context 里必须包含哪些可验证事实(人力、性能目标、皮肤需求、团队学习曲线)?你能接受的分阶段方案是什么?
3. 当前 Code Review 中,哪些评论其实应该由工具(clang-format/clang-tidy/CI)来回答?把它们自动化能省下多少评审时间,这些时间应该投入到哪类评审上?
4. 画出你团队的技能矩阵(3–5 项能力 × 全部成员),找出至少两个单点风险,并各设计一个「结对 + 轮值评审」的培养动作与验收标准。
5. 如果架构必须跟着组织结构调整,那么未来半年你希望把哪块公共能力(构建发布 / UI 组件 / 崩溃上报)从交付团队里剥离成平台能力?这对团队的交互模式意味着什么?
目标:把「决策分级 + 必备字段」变成可执行的规则,而不是口头约定。程序对每条 ADR 判断它是单向门还是双向门,检查必备字段,给出应走的评审路径,并在缺字段时以非零退出码阻塞 CI。
代码(C++17,adr_gate.cpp,可直接编译):
#include <iostream>
#include <string>
#include <vector>
struct Adr { // 一条架构决策记录的精简视图
std::string id, title, door, status;
bool hasConsequences, hasAlternatives, hasRevisit;
};
std::vector<std::string> missingFields(const Adr& a) {
std::vector<std::string> m;
if (a.title.empty()) m.push_back("Title");
if (a.status.empty()) m.push_back("Status");
if (!a.hasConsequences) m.push_back("Consequences");
if (a.door == "one-way") { // 单向门:额外强制两项
if (!a.hasAlternatives) m.push_back("Alternatives considered");
if (!a.hasRevisit) m.push_back("Revisit trigger");
}
return m;
}
int main() {
const std::vector<Adr> adrs = {
{"0007", "Qt6 迁移", "one-way", "accepted", true, true, true },
{"0008", "日志库换 spdlog", "two-way", "accepted", true, false, false},
{"0009", "主界面改用 QML", "one-way", "proposed", true, false, false},
};
int blockers = 0;
for (const auto& a : adrs) {
const bool oneWay = (a.door == "one-way");
const auto miss = missingFields(a);
std::cout << "[" << a.id << "] " << a.title
<< " door=" << a.door << " status=" << a.status << "\n";
std::cout << " 评审路径: "
<< (oneWay ? "设计评审 + 唯一 Approver + ADR 入仓"
: "作者自决 + PR 留理由(事后通知)") << "\n";
if (oneWay && a.status != "accepted")
std::cout << " ⚠ 未决单向门:禁止开工,先补齐评审\n";
if (miss.empty()) {
std::cout << " 字段完整 ✓\n";
} else {
++blockers;
std::cout << " 缺失: ";
for (const auto& f : miss) std::cout << f << " ";
std::cout << "→ 阻塞合入\n";
}
}
std::cout << "门禁结果: " << (blockers ? "阻塞 " : "通过 ")
<< blockers << " 条待补齐\n";
return blockers ? 1 : 0; // 非零退出码即可接入 CI
}
CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(adr_gate LANGUAGES CXX)
add_executable(adr_gate adr_gate.cpp)
target_compile_features(adr_gate PRIVATE cxx_std_17)
运行(也可不用 CMake,直接编译):g++ -std=c++17 -Wall -Wextra -O2 adr_gate.cpp -o adr_gate && ./adr_gate; echo "exit=$?",或用 Qt 项目一贯的 cmake -S . -B build && cmake --build build。
进阶(各 10 分钟,任选其一):
adrs 换成读取 docs/adr/*.md,用 std::ifstream + std::regex 抽取 YAML 前置区的 door/status 与正文中的 ## Consequences / ## Alternatives / ## Revisit trigger 标题。