不止于代码
成为一名优秀的软件工程师不仅仅是写出能运行的代码。 你还需要写出其他人(包括未来的你)能看懂、能维护、能继续构建的代码。 你需要清晰沟通、认真贡献,并在你参与的生态系统中——无论是开源还是闭源——做一个好公民。
单向沟通
软件工程的很大一部分,是为那些缺乏你当前背景和上下文的人而写作: 后面加入的队友、接手你代码的维护者,或者半年后的你——连当初为什么做某个决定都忘了。 所有这类写作的关键建议是:目标是记录并传达“为什么”,而不仅是“做了什么”。 “做了什么”通常一目了然,而“为什么”是来之不易、最容易随时间流失的知识。
也许除了代码本身之外,最常见的工程师之间的沟通形式就是代码注释。 我个人觉得很多注释毫无用处,但它们完全可以写得更好! 好的注释解释代码本身无法表达的内容:某件事为何要这样做,而不是它如何工作(代码本身已经展示了过程)。 好的注释能节省大量困惑,而糟糕的注释只会制造噪音,甚至误导读者。
几乎总是值得写的注释类型:
- TODO:标记尚未完成或尚未打磨的代码,但要留下足够的上下文,让别人知道还缺什么、为什么被延期。“TODO: optimize”毫无价值;而“TODO: 这段 O(n²) 循环在
n<100时没问题,规模放大后需要索引”可行。 - 参考资料:当代码实现论文里的算法、借鉴外部代码或遵循文档规定的行为时,给出外部链接。使用永久链接,并注明与参考实现的差异。
- 正确性说明:解释为什么不寻常的代码能产生正确结果。代码显示了步骤,注释说明这些步骤为何奏效。
- 血泪教训:如果你花了 30 分钟以上才调通、且修复方式不明显,就把它记下来。过去的你不知道需要这一步,未来读者也不会知道。
- 常数的理由:魔法数字也需要解释。为什么是 1492?为什么是 16 位?它是随手选的、测试得出的还是正确性所需?即便是“随意选的”也是有用信息。
- 承重细节:如果正确性依赖一个看似无关紧要的实现细节(例如“必须是 BTreeSet,因为下面迭代顺序有要求”),务必点出来。
- “为什么不用”:当你刻意避开显而易见的做法时,说明理由。 否则总有人后来“修复”它并把东西搞坏。
README(你应该写了吧?)也是其他开发者的常见初次接触点。 好的 README 会立刻按顺序回答以下四个问题:它做什么?我为何要在乎?如何使用?如何安装? 像漏斗一样组织:顶部用一句话描述、也许再加一个视觉示意,让人几秒内判断它是否解决自己的问题,然后逐步增加细节。 先展示用法再讲安装——人们想先看看能得到什么,再决定是否投入安装步骤。
提交信息(commit message)也是一种常被忽视的“写给别人看的”内容。
它们经常写成“fix blah”或“add foo”,虽然有时够用,但我们常忘了它们构成了代码库演进的历史记录——记录的是“为什么”。
当有人(包括你)运行 git blame 想弄懂某个困惑的改动时,好的提交信息应该能解答他们的问题。
一般来说,提交正文应该回答:
- 是什么问题迫使我们改动?
- 考虑过哪些备选方案?
- 这个方案的取舍或影响是什么?
- 有哪些可能让人意外的点?
当然,详略要与复杂度匹配。 一行错别字只需要主题行;修一个花了数小时排查的竞态条件,就值得用段落解释问题和解决方式。
对于复杂改动,可以采用“问题 → 解决方案 → 影响”结构: 先讲迫使你做改动的限制或需求,再讲改了什么、关键设计决定,最后列出值得注意的后果(正反面都要提)。 最后一部分尤其重要:真实的工程是各种平衡;记录下某个取舍是刻意为之,可以防止后来的人以为你忽略了问题。
LLM 可以帮忙写提交信息。 不过,如果你只是把改动丢给 LLM 让它写提交,它只能看到“做了什么”,看不到“为什么”,结果只会是描述性的(正好与我们想要的相反)。 如果你一开始就是用 LLM 协助完成改动,那么在同一会话里让它写提交就好得多,因为你和它的对话天然包含了丰富上下文! 否则或此外,一个实用技巧是:明确告诉 LLM 你想要聚焦“为什么”的提交信息(以及上述其他要点),然后要求它向你提问缺失的上下文。 本质上,你就像是这个编码代理的一个 MCP“工具”,供它“读取”上下文。
随着改动变复杂,务必把提交也拆得逻辑清晰(git add -p 会是你的好帮手)。
每个提交都应该代表一个连贯的改动,可以独立理解和评审。
不要把重构和新功能混在一起,也不要把不相关的 bug 修复塞在同一个提交里——这会搅浑故事,让人弄不清是哪个改动解决了哪个问题,也几乎肯定会拖慢评审。
而且这样做还能让你在 git bisect 时拥有超能力,但这是另一个话题了。
当你开始更认真地对待技术写作、并更频繁地使用它时,记得尊重读者。 一旦养成习惯,很容易过度解释,但你必须克制,否则读者反而一句都不会看。 解释“为什么”,相信他们会为自己的情境搞清楚“怎么做”。
协作
作为工程师,我们可能有不少时间独自敲代码,但也有相当一部分时间在与他人沟通。 这些时间通常分成协作和教育,而提升这两方面的回报都非常可观。
贡献
无论你是在提交 bug 报告、贡献一个小修复,还是实现一个大型特性,都要记住:通常用户数量远大于贡献者,而贡献者数量又远大于维护者。 因此维护者的时间极为稀缺。 想让你的贡献更可能发挥作用,你就需要确保它信噪比高,值得维护者投入时间。
比如,一个好的 bug 报告会尊重维护者的时间,提供理解和复现问题所需的一切:
- 环境:操作系统、版本号、相关配置
- 期望的结果 vs 实际发生的情况
- 复现步骤:越具体越好。 “点击按钮”不如“以管理员身份登录 /settings 页面,点击 Submit 按钮”有效。
- 你已经尝试过什么:这能避免重复建议,也表明你做过初步调查。
如果你发现安全漏洞,不要公开发布。 先私下联系维护者,给他们合理时间修复。 许多项目会有 SECURITY.md 或类似文件说明流程。
务必搜索已有的问题。 你的 bug 或特性请求可能已经有人提过。 相比创建重复条目,往现有讨论里补充信息要好得多,也能减少维护者的噪音负担。
如果能提供最小可复现示例,那简直是黄金。 它能为维护者节省大量时间和精力,而可靠复现 bug 往往是修复过程中最难的部分。 更别提,你在隔离问题时付出的努力常能帮你更深入理解问题,有时甚至会引导你找到修复方案。
如果暂时没收到回复,记住维护者往往是时间有限的志愿者。 如果你等他们回复,一两周后礼貌跟进一次没问题;每天催就不行。 同样,“我也是”的评论,或只是粘贴一大段终端输出的 bug 报告,往往会让你的问题更难得到关注。
如果你准备提交代码贡献,也要熟悉贡献指南。
许多项目都有 CONTRIBUTING.md——一定要遵守。
通常也建议从小处开始:修个错别字或改进文档是很好的首次贡献,它能让你熟悉项目流程,而不用在内容本身上来回拉锯。
注意项目使用什么许可证,因为你贡献的代码会受同样的许可证约束。 特别要留意像 GPL 这样的强制开源(copyleft)许可证,它要求派生作品也开源,可能会影响你的雇主。 choosealicense.com 上有更多实用信息。
当你决定发起一个 Pull Request(“PR”)时,首先确保你隔离了真正想被接受的改动。 如果 PR 同时改了很多无关内容,审核人很可能会退回让你整理。 这和你应该如何把 git 提交拆成语义相关的块是类似的。
在某些情况下,如果你有许多看似不相关但都是为某个特性铺路的改动,那么开一个较大的 PR 也可能可以。 不过在这种情况下,提交的整洁度(commit hygiene)就尤为重要,这样维护者可以选择“按提交”逐步审阅。
接下来,务必把改动背后的“为什么”说明清楚。
不要只描述“改了什么”——要解释“为什么需要改”以及“为什么这是解决问题的好方法”。
如果有需要特别关注的部分,也要主动指出。
根据 CONTRIBUTING.md 和改动性质,审核者可能还会期待看到取舍说明或测试方法等附加信息。
我们建议优先向上游项目贡献,而不是“直接分叉(fork)”。 (在许可证允许的前提下)分叉应该留给那些确实超出原项目范围的改动。 如果你确实需要分叉,记得感谢原项目!
AI 让我们很容易快速生成看似靠谱的代码和 PR,但这不意味着你可以不理解自己贡献的内容。 提交你解释不了的 AI 生成代码,会让维护者承担审查甚至维护连作者都不懂的代码的负担。 可以使用 AI 帮你定位问题、产出修复或特性,前提是你仍要尽到应有的打磨责任,而不是把这份工作甩给(已经过载的)维护者。
记住,对维护者来说,接受 PR 就意味着承接长期责任。 贡献者可能很快就离开了,但维护者还要一直维护这段代码。 因此他们可能会拒绝那些出于好意但不符合项目方向、增加他们不愿承担的复杂度、或需求本身并未充分论证的改动。 作为贡献者,要由你来说明为什么接受这份贡献值得维护负担。
当你收到 PR 反馈时,记住“代码不是你本人”! 审核者是在努力让代码更好,而不是批评你这个人。 如果不同意,提出澄清问题——也许你能学到东西,或者他们会学到东西。
评审
你可能认为代码评审是资深工程师才做的,但通常比你想象得要早就会轮到你,而且你的视角很有价值。 新鲜的眼睛能抓到资深开发者忽视的东西;不熟悉代码的人提出的问题,常能暴露那些应该被记录或简化的假设。
评审也是最快的学习方式之一。 你会看到别人如何解题,学到模式和惯用法,并逐渐培养对可读代码的直觉。 除了个人成长,评审还能在代码入库前捕捉 bug、在团队内传播知识,并通过协作提升代码质量。 它绝不只是官僚负担。
好的代码评审需要时间打磨,但有些技巧能立刻让评审更有效:
- 评审代码,而不是评审人: “这个函数读起来有点费解”比“你写的代码很难懂”更合适。
- 更倾向于给出可操作的建议: “这里能否改成用配置 dataclass,而不是全局变量?”比“别用全局变量”更容易处理。
- 提问而非命令: “如果这里 X 为 null 会怎样?”比“把 null 情况处理掉”更能促进讨论。
- 解释“为什么”: “这里用常量吧”不如“这里用常量,方便根据环境调整超时时间”来得有用。
- 区分阻断性问题和建议: 说明哪些必须修改,哪些只是偏好。
- 肯定做得好的地方: 指出巧妙的解决方式或干净的实现会让人受鼓舞,也能让作者知道哪些值得持续保持。
- 知道何时收手: 贡献者的时间和精力有限,不一定要花在所有细枝末节上。 把注意力放在大问题上,必要时可以自己事后顺手清理那些小问题。
AI 工具能抓到某些问题,但它们不能替代人工评审。 它们缺乏上下文、不理解产品需求,也可能一本正经地提出错误建议。 可以当作第一道筛查,但别指望它们取代深思熟虑的人工评审。
教学互助
作为工程师,我们大量非编码时间都在提问或回答问题,或两者混合:在协作中、与同事讨论时、或自己学习时。 会问问题是一项能让你向任何人学习的技能,不需要对方是完美的讲解者。 Julia Evans 有两篇很棒的博客文章值得一读:如何提好问题 和 如何获得有用的答案。
特别有价值的建议包括:
- 先陈述你的理解:先说你认为自己知道什么,然后问“这样对吗?”这有助于回答者定位你的知识盲区。
- 多用是/否问题: “X 是真的吗?”能避免跑题,通常也会引出有用的补充。
- 具体一点: “SQL join 是怎么回事?”太笼统;“LEFT JOIN 会包含右表没有匹配的行吗?”才容易回答。
- 承认自己不理解: 遇到不熟悉的术语就打断询问。这体现的是自信而非弱点。同样地,如果对方问你、而你不知道答案,最好直接说“我不知道”,然后可以补一句“不过我猜……”或“不过我可以去查”。
- 不要接受不完整的答案: 持续追问,直到你真正理解为止。
- 先做些调研: 基础调查能让你提出更有针对性的问题(不过同事间随口问问当然没问题)。
记住:精心设计的问题能让整个社区受益。它们会揭示其他人同样需要理解的隐藏假设。
注意,这些建议在和 LLM 沟通时同样适用!
AI 礼仪
随着 LLM 与 AI 在软件工程中的使用越来越多,相关的社会和职业规范仍在发展中。我们已经在agentic coding lecture里覆盖了许多战术层面的考虑,但它们的“软”使用方式也值得讨论。
首先是:当 AI 对你的工作有实质贡献时,要披露。这不是为了羞辱,而是为了诚实、设定合适的预期,并确保成果得到恰当程度的评审。同样有必要说明你在哪些部分使用了 AI——“整个都是 vibe coding 出来的”和“我写了这个备份工具,用 LLM 来设计 Web 前端样式”有很大区别。 比如,我们也使用 LLM 来协助撰写这些讲义,包括校对、头脑风暴、生成代码片段和练习题的初稿。
其次,要遵循你所在团队或贡献项目的规范。 有些团队对 AI 使用有更严格的政策(出于合规或数据驻留等等原因),你不想无意中违反。 对 AI 使用保持开放可以避免代价高昂的错误。
如果你的目标是通过正在做的工作来学习,记住让 AI 做掉全部或大部分工作有时会适得其反: 你可能更像是在学习如何写提示、如何审查 AI 输出,而不是学习任务本身。 尤其在学习阶段,重要的是过程而不是结果,用 AI “快速得到答案”反而违背初衷。
在面试等评估场景里也有类似顾虑。 这些场景往往是为了评估你的技能,而不是 LLM 的。 现在有更多公司允许你在面试中使用 LLM 或其他 AI 辅助工具,只要你让他们观察整个交互过程(也就是说,他们会评估你使用这些工具的能力!),但这样的公司仍是少数。 如果不确定某个任务是否允许 AI 辅助,就问清楚!
毫无疑问,如果评估明确要求不使用外部工具、不使用 LLM,就不要用。 试图偷偷使用并不被发现,终究会出问题。
练习
-
浏览一个知名项目(例如 Redis 或 curl)的源码。 找出讲义中提到的几种注释类型:有用的 TODO、指向外部文档的引用、解释为何避开某种做法的“why not”注释、或血泪教训。 如果这些注释不存在,会失去什么?
-
选一个你感兴趣的开源项目,查看最近的提交历史(
git log)。 找到一个能解释“为什么要改”的好提交信息,再找一个只描述“改了什么”的弱提交信息。 对于后者,查看 diff(git show <hash>),尝试按照“问题 → 解决方案 → 影响”结构写一个更好的提交信息。 体会一下:事后补全背景信息是多么费劲! -
对比三个 1000+ star 的 GitHub 项目的 README。 它们都同样有用吗? 找出哪些内容在你看来主要是噪音,为你将来写 README 提供借鉴。
-
在一个你使用的项目中找一个未解决 issue(如果有的话,可以看“good first issue”或“help wanted”标签)。 根据讲义中的标准评估它:它是否尊重维护者的时间、包含所有必要信息,还是你预期维护者需要和提交者来回追问多轮才能找到根源?
-
想想你在使用的软件里遇到的一个 bug(或者在 issue tracker 中找一个)。 练习创建一个最小可复现示例:剥离所有与 bug 无关的东西,只留下仍能展示问题的最小案例。 写下你去掉了什么以及为什么。
-
找一个你熟悉项目里已经合并的 PR,其中包含实质性的评审意见(不只是“LGTM”)。 通读整个评审。 所有评论同样高效吗? 如果你是 PR 作者,收到这些评论的体验会如何?
-
打开 Stack Overflow,找一个你熟悉技术领域内获得高票回答的问题。 再找一个被关闭或大量被点踩的问题。 按照讲义的建议对比它们:能否预见哪个问题更可能得到好答案?
Licensed under CC BY-NC-SA.