💡 Key Takeaways
- Rule I Follow #1: Functions Should Do One Thing (But That Thing Can Be Bigger Than You Think)
- Rule I Follow #2: Names Should Reveal Intent (Even If They're Long)
- Rule I Follow #3: Comments Explain Why, Not What (But Use More Than You Think)
- Rule I Follow #4: Fail Fast and Fail Loud
作为一家每月处理23亿美元交易的金融科技公司的资深软件架构师,我已经分析了其他人的代码14年了。上周二,我审查了一个让我身体感到不适的拉取请求——在一个函数中有847行,变量名如“data2”和“temp”,评论中甚至写着“// 这里发生魔法”。写这个代码的开发者?斯坦福大学计算机专业毕业生,GPA 3.9。
💡 关键要点
- 我遵循的规则 #1:函数应该做一件事(但那件事可能比你想的要大)
- 我遵循的规则 #2:名称应该揭示意图(即使它们很长)
- 我遵循的规则 #3:评论解释为什么,而不是做什么(但多用也很重要)
- 我遵循的规则 #4:快速失败,大声失败
这时我意识到:我们一直在错误地教授干净代码。
每个人都像引用《清洁代码》的罗伯特·马丁(Uncle Bob)一样,引用得理所当然。他们背诵 SOLID 原则,争论制表符与空格,写出小得需要显微镜阅读的函数。可是没有人告诉你:其中一些规则正在主动让你的代码变得更糟。
我不是来抨击罗伯特·马丁的工作的——它是基础且重要的。但在审查了超过3000个拉取请求,辅导了47名开发者,并维护一个自2011年以来一直在生产中的代码库后,我学会了哪些规则实际上重要,哪些只是开发者表演。让我告诉你我的意思。
我遵循的规则 #1:函数应该做一件事(但那件事可能比你想的要大)
函数的“单一职责原则”可能是干净代码中最被误解的规则。我看到开发者创建的功能仅三行长,名称如“validateUserEmailFormat”,被“validateUserEmail”调用,再被“validateUser”调用。这可真是层层相扣,你需要打开七个文件才能理解用户注册时发生了什么。
我实际做的是:我编写完成一个有意义的业务操作的函数,而不是一个技术操作。当我写了一个名为“processPayment”的函数时,它可能有45行。它验证支付方式,检查欺诈,创建交易记录并发送确认邮件。这是四个技术操作,但这是一个业务操作:处理付款。
关键是这些步骤在代码中清楚地划分。我使用空行分隔逻辑部分,并添加简短的评论,解释每个部分的“为什么”。当其他开发者阅读“processPayment”时,他们可以理解整个付款流程,而无需在十二个不同的文件之间跳转。
我曾经对此进行过测量。在我们旧的代码库中,我们虔诚地遵循“函数必须小”的规则,平均开发者需要打开8.3个文件才能理解单个用户流程。在我重构为“有意义的操作”函数后,这个数字降至2.1个文件。代码审查时间减少了34%。错误修复时间减少了28%。
规则不是“让函数变小”。规则是“让函数可理解”。有时候这意味着10行。有时候这意味着50行。但它绝不会意味着强迫开发者在你的整个代码库中游走,只为搞清楚一个按钮点击是怎么工作的。
我遵循的规则 #2:名称应该揭示意图(即使它们很长)
我曾和一个开发者合作,他坚称变量名称绝不能超过15个字符,因为“这会使代码更难阅读”。他写了这样的代码:“const usrPmtMthd = getUserPaymentMethod();”我真想把键盘扔出窗外。
“最好的代码不是最聪明的——而是下一个开发者在凌晨2点系统崩溃、客户打电话时能理解的代码。”
我的规则是:如果我通过一次阅读变量名不能理解它包含的内容,那么这个名称就是错误的。我不在乎它是不是40个字符长。与其花三分钟去弄清楚“pmtMthd”是什么意思,我宁愿阅读“userSelectedPaymentMethodFromDropdown”。
在我们的支付处理系统中,有一个变量叫“transactionRequiresAdditionalFraudVerificationBasedOnUserHistory”。它有71个字符,但非常清晰。当你在一个if语句中看到这个变量时,你就确切知道在检查什么以及为什么。不需要评论,不需要精神翻译。
我总是听到的反驳是“但是长名称使行太长!”当然,如果你还在假装我们是在1985年的80列终端上写代码的话。我们现在有27英寸的显示器。利用它们。我将我的行长度限制设置为120个字符,从未有过可读性问题。
这是我使用的测试:如果一名初级开发者能阅读你的变量名称并立即知道它包含什么,类型是什么,以及大致用作何目的,那么你就命名正确了。如果他们需要向上滚动以查看它的声明位置或检查类型定义,那么你就失败了。
我看到这显著提高了代码审查质量。当名称清晰时,审查人员花在问“这是什么”的时间更少,而在问“这是否正确的方法”的时间更多。那才是真正的价值所在。
我遵循的规则 #3:评论解释为什么,而不是做什么(但多用也很重要)
干净代码的纯粹主义者会告诉你“好代码不需要评论”。他们说对了一半。好代码不需要解释代码做什么的评论,但它绝对需要解释代码存在为什么的评论。
| 清洁代码规则 | 书籍所说 | 实际有效的方法 | 何时打破它 |
|---|---|---|---|
| 函数长度 | 保持函数在20行内 | 保持函数在一个屏幕内(40-60行) | 当拆分造成的困惑多于清晰时 |
| 评论 | 代码应自我文档化 | 解释为什么而不是什么 | 复杂算法、业务规则或不明显的决策 |
| DRY原则 | 绝不要重复自己 | 在第三次出现之前不要重复自己(等待第三次出现) | 当抽象将无关的特性耦合在一起时 |
| 变量名称 | 始终使用描述性名称 | 上下文很重要:在循环中使用'i'没问题,在小范围内使用'userAuthenticationToken'则是多余的 | 循环计数器、领域上下文中的熟知缩写 |
上个月,我在我们的代码库中发现了一个函数,它有一个奇怪的解决方案:在处理网络钩子之前添加了50毫秒的延迟。没有评论。没有说明。只有“await sleep(50);”在那里像地雷一样。我花了两个小时试图弄清这个是一个bug还是故意的。结果发现这是一个临时解决方案,用于解决我们在生产中调试三天后发现的第三方API中的竞态条件。
现在那段代码有了评论:“// 需要50毫秒延迟:Stripe 的网络钩子可能会在其API反映费用状态之前到达。发现于事件#2847,时间:2023-08-15。在Stripe修复其最终一致性问题后删除此内容(工单#45892)。”
这是一条好评论。它解释了代码存在的原因,引用了导致它的事件,并且提供了未来删除的路径。如果没有这条评论,下一个开发者可能会误以为这是个错误而删除它。
在以下三种情况下我会自由地添加评论:当我在依赖项中的bug周围工作时,当我实现一个不明显的业务规则时,以及当我出于性能优化而使代码可读性降低时。在每种情况下,评论解释了在代码中看不到的上下文。