C++/Qt | 33 个库,我不敢一次改完 35 工程师的核心竞争力之一让别人敢在周五下班前合并你的改动一、周五下午五点半改造进行到第三周。那天周五下午五点半我坐在工位上手里有一个改动把 11 个服务从静态库改成动态插件。C17、Qt6、MSVC x642000 个左右的源文件。我盯着编译按钮看了大概五分钟。然后我把手从鼠标上拿开了。不是不会点。是我不知道点下去之后如果炸了我能不能在十分钟内退回到一个能交付的状态。那天我最后没有点。我把改动全部撤销回滚到昨天那个能跑的版本然后关电脑回家了。回家的路上我想明白了一件事35 岁之前我最怕的是「技术做不出来」。35 岁之后我最怕的是「做出来了但没人敢用」。这两种恐惧指向的是两种完全不同的能力。前者靠学习后者靠交付确定性。二、为什么 35 岁之后交付确定性的权重更高年轻的时候一个项目做不完你可以靠加班补回来。因为你的成本主要是体力而体力在 25 岁到 35 岁之间是一条明确下行的曲线。到了 35 岁你能加码的东西变了25 岁的三个杠杆 35 岁的三个杠杆 ────────────── ────────────── 加班 分批决策 多学点新框架 判断哪些不用学 硬扛 让人愿意跟你一起干加班是有上限的。判断没有。这就是为什么我认为 35 工程师应该把「交付确定性」当成核心竞争力去练而不是等到需要时才临时抱佛脚。交付确定性说白了就一句话我交给你的东西你可以放心地用。这句话在团队里的成本最低、收益最高。三、❌ 大爆炸重构 vs ✅ 分批改造3.1 问题的真实规模先说清楚我面对的是什么。AutoPlatform 这个项目里common/下面有一批孤儿静态库——编译出来了但一个都没被链接。审计报告清点出来是 33 个33 个孤儿静态库 ├── services/ 11 个 auth, alarm, audit, config, init, │ parameter, state, flow_runner, │ flow_property, localization, timed_task ├── business/ 7 个 production, quality, traceability, │ maintenance, check_list, │ golden_sample, export ├── data/ 3 个 repository, ifms, data_collector └── device/ 12 个33 个库跨 4 个模块。每一个都要抽出接口、注入Q_DECLARE_INTERFACE、改成 SHARED、补元数据、验证运行时加载。如果我一口气做完会发生什么3.2 ❌ 我最想干的那件事❌ 大爆炸式改造 第 1 天 改 services/ 11 个库 第 2 天 编译不过 → 找原因 第 3 天 又编译不过 → 换原因 第 4 天 改 business/ 7 个库 第 5 天 链接冲突找不到符号 第 6 天 怀疑是设备层的问题 第 7 天 回滚试试回滚不干净新旧机制混在一起了 第 8 天 业务需求在排队我不敢再碰 第 9 天 这个任务变成了一个我不敢打开的分支问题不在技术难度。问题在第 7 天那一行回滚不干净。新机制已经接上了一部分旧机制还在原地。这时候回滚不是「撤销」是「再改一次」。从那一刻起我失去的不是进度是确定性。3.3 ✅ 我改成的做法✅ 分批改造 第一批services/ 里挑 5 个最简单的config, init, parameter, localized, state 之类无复杂依赖的 → 一个独立提交 → 编译 → 跑 → 确认 → 提交号 b3772f6 第二批services/ 剩下 6 个 → 一个独立提交 → 验证 → 提交号 07f4122 第三批business/ 7 个 → 先给接口注入声明再转换 → 提交号 02978e8 第四批data/ 3 个 → 提交号 67182a0 device/ 12 个驱动单独排期因为它们还牵扯设备型号开关每一批的共同点1. 独立提交 → 出问题能精确回退 2. 能独立编译通过 → 不依赖下一批才能验证 3. 能独立说明白 → 我讲得清这一批做了什么 4. 一周内能交付 → 不是我一个人闷头憋两周四、小步的三层价值我原来只看到第一层一开始我以为分批只是为了「更安全」。后来我发现不是。分批真正的价值有三层而且后两层比第一层值钱得多。4.1 第一层可回滚这是最低级的价值大爆炸改造失败 → 不知道退到哪 分批改造失败 → 退到上一个提交点 就这一点已经值很多了。但这只是保命不是收益。4.2 第二层可讲解这一层我很晚才意识到。分批之后我第一次能对着人说清楚「服务层我已经改完了11 个分两批每批一个提交。业务层 7 个改完了。数据层 3 个也改完了。设备层 12 个还没动因为它们受设备开关体系影响我打算单独排。」这段话本身就是一个交付物。对比大爆炸改造时期的说法「我在改插件化改了一些还有些没改完不好说进度。」❌ 「我在改改了一些」 → 别人无法评估无法信任 ✅ 「11 个改完7 个改完3 个改完12 个没动」 → 别人知道现在在哪对 35 的工程师来说「可讲解」是一种被严重低估的能力。年轻时候你交付代码就行因为你在团队里的位置是执行者。35 岁以后你的交付对象变成了别人的信心。而信心是靠信息透明度建立的。4.3 第三层可交付这层最有价值也最反直觉。一个 33 个库的改造任务 如果按大爆炸做 → 交付时间不可预测 → 我永远不知道哪天能交差 如果分批去做 → 每周都有东西可以交 → 每周都有明确的完成感我 35 岁之后最怕的不是忙是忙了三个月但看不到交付物。那种感觉会持续侵蚀你的判断力你开始怀疑方向开始为了进度而妥协代码质量最后交付一个自己都不敢签字的东西。分批改造本质上是在对抗这种自我怀疑。小步的价值不只是安全是让一个可能永远做不完的工程变成每周都能看到进展的小工程。4.4 一个必须承认的现实分批也有代价我要如实说分批期间新旧机制是并存的。审计报告里对这个状态的评价是「语义不明」——是故意保留的过渡期还是没改完留下的残留光看代码判断不出来需要业务侧确认。这个中间态本身就是一种不干净。而它的存在成本就是每个新人进来都要花时间搞明白「这为什么有两套」。我没有假装这个问题不存在。我把它写进了改造记录并且给每一批都留了明确的提交边界让后来的人能沿着提交号倒着看。分批不是没有代价是把代价从「不可控」变成「可解释」。五、一个假开关值得单独一节我前面提过DEVICE_*_BUILD死选项。放在「交付确定性」这个话题下它值得单独解剖。5.1 事件的完整经过接手项目的人想启用某个设备型号的驱动。文档写着-DDEVICE_ZG13_BUILDON 即可启用 zg13 驱动的编译他照着做。编译通过。产物里没有这个驱动。运行时依然不加载。他改了十几次参数每次都重新编译。半小时后他来找我说「这开关是不是坏了」。我说不是。这个开关根本就是死的。审计报告里的原话是文档说能开实际构建行为纹丝不动。5.2 一个假开关是怎么活下来的我觉得这件事比任何技术难点都更值得研究。第一步有人设计了一组 DEVICE_*_BUILD 开关 intending 支持多型号 第二步某次重构里让这组开关不再真正生效可能是条件写错 可能是变量作用域改了可能只是没人注意 第三步没人发现——因为默认只有一个型号MTFTest在编译 不开这个开关也不影响日常工作 第四步文档没跟着改或者改了但没人核对 第五步它就一直躺在那里伪装成一个功能注意第三步一个死开关能活下来往往是因为它恰好不影响主线工作。这太常见了。我见过的技术债里相当一部分是这样活的东西看起来存在 → 日常主线用不到 → 没人验证 → 一直存在5.3 它的真实成本一个假开关消耗的不是 CPU也不是内存。是团队对文档的信任。第一次 我怀疑自己配错了 第二次 我怀疑这个项目的文档 第三次 我再也不相信文档只相信我亲手验过的东西第三步是不可逆的。一旦团队不再相信文档你就回到了最原始的工作方式所有事情都要自己验证一遍。对一个 2000 个源文件的项目这意味着每次开发都要额外付出巨大的成本。而这个成本是隐形的没人会在复盘会上算它。5.4 根治动作我最后做的事很简单但花了时间1. 把所有 DEVICE_*_BUILD 相关的文档说明删掉 —— 不是修正它是先让它不再说谎 2. 把这组开关从构建系统里彻底移除 —— 留着假开关本身就是在给未来埋雷 3. 在改造记录里写清楚为什么删 —— 半年后有人问「原来不是支持多型号吗」 记录里能查到当时的判断和限制重点是第 3 步。删掉一个假开关如果什么都不说它会在半年后以「这里本来应该支持 X」的形式重新出现。这就是知识资产的必要条件动作要有记录记录要有理由。六、复盘写什么不要汇报成绩写复盘这件事我也在改。6.1 ❌ 我以前写的那种复盘❌ 典型内容 本次改造顺利完成共转换 11 个服务插件 全部编译通过运行时加载正常效率提升明显。这段话有个致命问题半年后没人看得懂它。它没有回答任何一个后来者真正会问的问题为什么先改 services不先改别的你当时排除了哪些方案为什么排除什么情况下这套做法会失效现在回头看哪一步你会改6.2 ✅ 现在我写的那种✅ 现在写的东西 【背景】 33 个孤儿静态库审计报告 §5.2 发现 B。 目标是让服务层能被真正动态加载。 【当时的约束】 - 设备层 12 个驱动受 DEVICE_*_BUILD 开关体系牵制 如果一起改开关问题会混进来无法定位 - 业务方不能接受超过一周的停滞 - 我对运行时插件加载的机制只有理论把握没有实战 【我放弃的方案】 一次性全改。放弃原因一旦失败 新旧机制并存后回滚成本极高我无法在当天恢复可交付状态。 【实际做法】 先 services/ 5 个最简单的打样b3772f6 验证接口注入方式可行再做剩下 6 个07f4122 然后 business/ 7 个02978e8最后 data/ 3 个67182a0。 【现在的已知问题】 - 分批期间新旧机制并存语义不明需业务侧确认哪些是过渡期 - 14 个插件 DLL 中仍只有 12 个被真正动态加载 剩下 2 个的原因没查清 【什么条件下应该重来】 如果需要支持同一进程内多实例 或插件需要热插拔这套方案需要重新设计。第二种写法更长但它有三个不可替代的东西1. 写清了「当时的约束」 → 半年后的人能理解你为什么这么选 2. 写清了「我放弃了什么」 → 让人知道你考虑过替代方案 3. 写清了「什么时候该重来」→ 防止这套做法被当成永恒真理复盘不是汇报成绩是给半年后的自己留一个可以推翻自己的理由。这句话我是从踩坑里学到的。我看过一份三年前的架构文档理由写得非常充分逻辑无懈可击。但它有一个致命前提那个前提今天已经不成立了。当时合理的选择在条件变化后就是错误。而如果你不写清条件后人就会把那个选择当成不可动摇的规矩继续沿用直到撞墙。七、交付确定性的三个信号那么怎么判断一次交付是「确定的」我自己只看三个信号┌────────────────────────────────────────────────────┐ │ │ │ 信号 1可回滚 │ │ │ │ 你的改动能不能在 10 分钟内退回到上一个可交付状态 │ │ │ │ → 能 → 放心交付 │ │ → 不能 → 你交出去的不是代码是风险 │ │ │ ├────────────────────────────────────────────────────┤ │ │ │ 信号 2文档一致 │ │ │ │ 文档里写的每一条现在还成立吗 │ │ │ │ → 成立 → 别人不会浪费时间 │ │ → 不成立 → 你在消耗团队的信任死选项的教训 │ │ │ ├────────────────────────────────────────────────────┤ │ │ │ 信号 3决策可追溯 │ │ │ │ 半年后的你能看懂今天为什么这么决定吗 │ │ │ │ → 能 → 这是可积累的资产 │ │ → 不能 → 半年后你要重新踩一遍或者更糟 │ │ 后人把它当成规矩继续沿用 │ │ │ └────────────────────────────────────────────────────┘三个信号里我最看轻的其实是第三个而它恰恰最重要。前两个是当下的确定第三个是长期的确定。一个项目死掉通常不是因为当下没交付而是因为没人说得清当初为什么这么做。到了那时候代码还在能跑但没人敢动——因为动了不知道会发生什么。这才是最贵的成本一个还在运行、但没人敢碰的系统。八、写在最后那天下午五点半我最后没有点那个编译按钮。回家的路上我想如果这次是我一个人加班做完、没人review、没人等我合并我大概会点。因为那时候的压力只在我自己身上我只需要骗过自己。但一旦我的改动要进入一个共享的分支一旦别人要在它之上继续写代码我的确定性就成了别人的成本。我后来终于明白35 岁之后我引以为傲的东西不是「我能搞定多难的技术问题」。是我交的东西别人敢直接用。这件事没有技术含量。但它是所有技术的前提。本期互动你上一次「不敢点编译按钮」是什么时候当时缺的是什么你的项目里有几个开关是「文档说能用其实不能」你的提交是小步的还是攒一大坨再提的欢迎在评论区聊聊。 评论区聊聊我想问一个更具体的问题你上一个项目的文档现在还成立吗我这边的答案是有一部分已经不成立了但没人去改。原因是「改文档」这件事永远排在「改功能」后面于是它就永远不会被改。如果你也有类似的、已知失效但没人更新的文档评论区说说它在哪儿。我们可能都需要一次「文档体检」。 觉得有用记得收藏 转发如果你也在维护一个共享分支、一个必须被别人信任的仓库这篇也许值得收藏。转给那个「总是一口气改完再提」的朋友。