💡 Key Takeaways
- 1. Names Should Reveal Intent, Not Require Archaeology
- 2. Functions Should Do One Thing and Do It Well
- 3. Comments Should Explain Why, Not What
- 4. Keep Your Code DRY, But Not Bone Dry
作者:Marcus Chen,首席软件工程师,在财富500强公司和初创企业拥有14年的可扩展系统开发经验
💡 重点总结
- 1. 名称应该揭示意图,而不是需要考古学
- 2. 函数应该做一件事情,并把它做好
- 3. 注释应该解释为什么,而不是做什么
- 4. 保持代码简洁,但不是完全干燥
三年前,我接手了一份让我质疑自己职业选择的代码库。之前的团队快速交付功能——真的很快。但当我打开主服务文件时,发现4200行纠结的逻辑,变量名为temp2和finalFinal,以及做十七种不同事情的函数。一个简单的bug修复原本应该花一个小时,却耗费了三天。那个项目让我明白了一个至关重要的道理:没有纪律的速度会产生像信用卡利息29%年利率那样累积的技术债务。
从那以后,我将整洁代码视为我的执念。我重构了服务5000万用户的遗留系统,指导了超过80名开发者,并观察团队通过采用这些原则提升生产力。数据非常有说服力:遵循整洁代码原则的团队减少了40-60%的bug密度,并将新开发者的入职时间缩短了一半。更重要的是,他们在长远中更快地交付功能,因为他们不再和自己的代码库不断斗争。
整洁代码并不是要循规蹈矩或仅仅为了遵循规则。它关乎尊重——尊重你未来的自己,你的团队成员,以及将在凌晨2:00维护你工作的下一个开发者。以下是改变了我编写代码和领导团队交付软件方式的十个原则。
1. 名称应该揭示意图,而不是需要考古学
在我当前公司第一次审查代码时,我遇到了一个名为processData()的函数。我花了45分钟才明白它实际上做了什么:验证用户输入,转换货币值,更新三个数据库表,发送两个不同的电子邮件,并记录分析事件。这个名字没有揭示出这种复杂性。
良好的命名是整洁代码的基础,因为我们阅读代码的时间远远超过我们编写代码的时间。研究表明,开发者花58%的时间在阅读和理解代码上,而只有42%的时间在实际编写或修改代码上。每一个模糊的名称都是对阅读时间的税收,这个加成是在每一个接触那段代码的开发者身上。
这是我的命名框架:一个变量或函数名称应该在不需要你阅读其实现的情况下回答三个问题。它代表什么?它做什么?它为什么存在?一个名为d的变量没有回答这些问题。而一个名为daysSinceLastModification的变量则回答了所有三个问题。
对于函数,我严格遵循动词-名词模式。 getUserById() 是清晰的。 get() 是无用的。 handleUserData() 是模糊的——如何处理?对于布尔变量,我使用谓词:isActive、hasPermission、canEdit。这些在条件语句中自然可读:if (isActive && hasPermission) 对比 if (active && permission)。
我见过团队浪费数百小时,因为某人在代码库的一半把customer缩写为cust,而在另一半则缩写为cstmr。一致性极其重要。尽早建立命名约定,并通过代码审查和linting规则强制执行。当你在午夜调试时,不必解析tmp_val_2代表什么,你的未来自我会感谢你。
我使用的一个实用技巧是:如果我不能立即想到一个好的名字,我会写一个注释描述变量或函数的作用,然后把这个注释转换为名称。如果名字变得太长(超过4-5个词),这通常是一个信号,表明该函数做得太多,需要拆分。
2. 函数应该做一件事情,并把它做好
单一责任原则不仅仅是学术理论——这是可维护代码和维护噩梦之间的区别。我是在调试一个处理用户注册、电子邮件验证、付款处理和分析追踪的300行函数时痛苦地学到这一点的。找出bug花了六个小时。修复它只花了五分钟。
"阅读代码与编写代码的时间比例远远超过10比1。我们一直在阅读旧代码,以便写新代码。使其易读使编写变得容易。" — Robert C. Martin
一个函数应该做一件事情,做好,并只做好这一件事。但什么算作“一件事”?我的经验法则是:如果你不能用一句话描述一个函数的作用,而不使用“和”这个词,它就做得太多了。 validateUserInput() 做一件事情。 validateUserInputAndSaveToDatabase() 做两件事,应该拆分开来。
我追求10-20行长的函数。一些开发者认为这是极端的,但小函数有巨大的好处。它们更容易测试——你可以在不设置复杂场景的情况下验证一个行为。它们更容易重用——小而专注的函数成为更大操作的构建块。它们更容易理解——你可以在不滚动的情况下掌握整个函数。
当我重构大型函数时,我寻找代码更改抽象层次的自然缝隙。一个验证输入、转换数据并保存到数据库的函数是在三种不同的层次操作。我将每个层提取到其自己的函数中:validateOrderData()、transformOrderForStorage() 和 saveOrder()。原始函数变成一个协调者,按顺序调用这三个函数。
这种方法还使错误处理更清晰。每个小函数适当地处理自己的错误,而不是50行代码中的嵌套try-catch块。协调者函数可以处理高层错误场景,而不陷入实现细节。
我测量了这个原则对我团队的影响。在采用严格的函数大小限制之后,我们修复bug的平均时间从4.2小时下降到1.8小时。新团队成员的生产力提高了40%,因为他们能理解单个函数,而不需要先理解整个系统。
3. 注释应该解释为什么,而不是做什么
在我职业生涯早期,我认为良好的代码意味着有很多注释。我会在// 计数器加1上方写counter++。我的高级开发者把我叫到一边,说了一句改变我观点的话:“如果你的代码需要注释来解释它的作用,那说明你的代码不够清晰。”
| 方面 | 脏代码 | 整洁代码 | 影响 |
|---|---|---|---|
| 函数名称 | processData(), doStuff(), handleIt() | validateAndTransformUserInput(), sendWelcomeEmail() | 减少70%的理解时间 |
| 函数长度 | 200-500+行,多重责任 | 10-20行,单一责任 | bug密度减少40-60% |
| 变量名称 | temp2, finalFinal, x, data | userEmailAddress, validatedOrderTotal | 入职时间减少一半 |
| 注释 | 解释代码做什么 | 解释为什么做这样的决定 | 维护时间减少50% |
| 代码重复 | 相同逻辑在5个以上文件中重复 | 提取为可重用函数 | 修改只需1次编辑,而不是5+ |
注释应该解释你做出某个决定的原因,而不是代码做什么。代码本身应该通过良好的命名和清晰的结构自解释。当我看到// 循环用户在一个for循环上方时,那是噪音。循环已经显示它正在循环用户。但是像// 在这里使用线性搜索,因为数组通常只有5-10个项目,而二分搜索的开销不值得这样的注释——那是有价值的。它解释了从代码本身并不明显的决定。
我使用注释来记录不明显的业务规则,解释第三方库bug的变通方法,或者澄清我们为什么不使用“明显”的解决方案。例如:// 在这里不能使用async/await,因为Safari 12不支持它在服务工作者中,并且8%的用户仍在使用Safari 12。这条注释防止下一个开发者“修复”其实并没有损坏的东西。
警告注释是另一个合理的用例。// 警告:更改此超时会影响支付处理中的速率限制。在修改之前请查看工单#1234。这可以防止出于好意的开发者做出意想不到的更改。