💡 Key Takeaways
- Rule I Follow #1: Functions Should Do One Thing (But I Define "One Thing" Differently)
- Rule I Follow #2: Meaningful Names Are Non-Negotiable
- Rule I Follow #3: Comments Explain Why, Not What
- Rule I Follow #4: Keep Functions and Classes Small (With Nuance)
作为一家中型金融科技公司的高级软件架构师,我已经盯着别人的代码14年了,我可以告诉你我何时停止成为一个干净代码狂热者:2019年3月的一个星期二,凌晨2:47,当我们的支付处理系统因为有人花了三天时间重构一个完美正常的模块,以遵循《Uncle Bob's book》中的每一条规则而崩溃时。讽刺的是?这个bug是在“清理”过程中引入的。
💡 关键要点
- 我遵循的规则 #1: 函数应当做一件事(但我对“一件事”的定义不同)
- 我遵循的规则 #2: 有意义的名称是不可妥协的
- 我遵循的规则 #3: 注释解释为什么,而不是做什么
- 我遵循的规则 #4: 保持函数和类的小型(有细微差别)
那个晚上改变了我对代码质量的看法。我并不是说干净代码原则是错误的——远非如此。但在审查了超过10,000个拉取请求,辅导47个开发者,并发布23个主要产品版本后,我学到的是,教条式地遵循任何一套规则只是另一种形式的技术债务。有些干净代码规则是绝对的金科玉律。而其他的?顶多是依赖上下文,最糟糕的时候则是有害的。
这是我在生产代码中实际做的,更重要的是,我为什么这样做。
我遵循的规则 #1: 函数应当做一件事(但我对“一件事”的定义不同)
函数的单一责任原则可能是我虔诚遵循的最有价值的规则。但我在这里与教科书有所不同:我并不通过代码行数或操作次数来衡量“一件事”。我通过概念凝聚力来衡量。
上个季度,我审查了一个长8行的函数,但它在单一责任原则(SRP)上表现得极其糟糕。它验证用户输入,并记录验证结果,并更新一个缓存。在8行中塞入了三个不同的责任。与我上个月写的一个45行函数相比,该函数协调了一个复杂的数据库事务——它做的是“一件事”(完成支付交易),但这件事情需要多个步骤连在一起。
这是我的试金石:我能否在不使用“和”这个词的情况下,用一句话描述这个函数的功能?如果我需要说“这个函数验证输入并发送电子邮件”,那它就做了两件事。但如果我说“这个函数处理退款请求”,并且这自然涉及验证、数据库更新和通知——那仍然是在正确的抽象层面上的一件事。
实际上,这意味着我的函数平均有25-30行,而不是纯粹主义者建议的10-15行。但我们这些函数的bug率比我们之前的过度抽象代码低40%。为什么?因为将相关操作放在一起可以减少理解系统的认知负担。当所有的东西被拆分成微小的函数时,你花更多的时间在文件之间跳转,而不是理解业务逻辑。
真正的胜利在于可测试性。做一件概念性事情的函数很容易测试,即使它有40行。你模拟依赖,调用函数,断言结果。完成。当你将所有东西提取成5行的函数时,你最终还是会得到集成测试,因为单元测试变得毫无意义。
我遵循的规则 #2: 有意义的名称是不可妥协的
我会坚定不移地坚持这一点:变量和函数名是你写的最重要的文档。我曾单纯因为命名不佳而拒绝拉取请求,我会再这样做。
“教条式地遵循任何一套规则只是一种技术债务的表现。最好的代码并不是最干净的——而是那些可靠发布并能被你的团队维护的代码。”
两个月前,一个初级开发者提交了一段代码,其中有一个名为`processData()`的函数。我把它退回,并附上了一段10分钟的Loom视频解释原因。这个函数专门是用来根据Luhn算法验证支付卡号的。正确的名称应该是`validateCardNumberChecksum()`。是的,它更长。是的,它更具体。这正是要点。
这是我经过数千次代码审查后,精炼的命名层次结构:
- 布尔变量:总是以is/has/can/should开头。不用`active`,而用`isActive`。不用`permission`,而用`hasPermission`。
- 函数:用于动作的动词,用于查询的名词。`calculateTotalPrice()`而不是`totalPrice()`。`getUserById()`而不是`user()`。
- 类:代表概念而非动作的名词。`PaymentProcessor`而不是`ProcessPayments`。
- 常量:对于真正的常量使用SCREAMING_SNAKE_CASE,对于可能更改的配置使用camelCase。
影响是可衡量的。在我们团队严格实施命名惯例18个月后,我们的平均PR审查时间从3.2小时降至1.8小时。为什么?因为审查人员花费更少的时间去解读代码的功能,而更多的时间在评估它是否正确。
我还执行了“无缩写”规则,除了恰好三个例外:`id`、`url`和`api`。其他一切都要完整拼写。`usr`变成`user`。`btn`变成`button`。`calc`变成`calculate`。在晚上11点调试时,额外的按键是值得的,这时不必猜测`tmpBfr`是什么意思。
我遵循的规则 #3: 注释解释为什么,而不是做什么
在我的职业生涯中,我见过两个极端:没有注释的代码库和每一行都有注释的代码库。这两者都错了,但过度注释的代码实际上更糟,因为它增加了维护负担,且通常是不真实的。
| 干净代码规则 | 何时遵循 | 何时忽视 | 现实世界影响 |
|---|---|---|---|
| 函数应保持简小 | 高流量代码路径,经常修改的模块 | 复杂的编排逻辑,事务处理 | 过早分割会造成导航负担 |
| 代码中无注释 | 自解释的业务逻辑 | 复杂算法,合规要求,非显而易见的优化 | 缺乏上下文会导致数小时的调试成本 |
| DRY(不要重复自己) | 核心业务逻辑,数据转换 | 外表相似但上下文不同的代码 | 过度抽象形成脆弱的依赖关系 |
| 避免原始痴迷 | 领域模型,API边界 | 简单的内部工具,性能关键路径 | 过度封装会增加认知负载 |
我的规则很简单:如果你在解释代码做什么,那么这段代码可能就不好。如果你在解释为什么做出特定决定,那就是一个好的注释。这里有一个真实的例子来自于