干净、干燥、扎实的意大利面

2025-05-24

干净、干燥、扎实的意大利面

看看你,你的项目已经准备好发布了!你对自己的风格和标准遵循度,以及对 DRY 和 SOLID 原则的严格遵守感到非常自豪。你已经完成了测试,bug 也已修复,用户文档和 API 文档也已完善。你的代码如此简洁,简直美轮美奂!

我有个坏消息。你的代码库可能仍然很糟糕。

那么,重点是什么?

需要说明的是,我并不反对我所描述的任何内容。TDD、DRY、SOLID(以及其他一大堆花哨的缩写)原则都有其适用之处。文档、标准和代码风格都很重要。如果你真的做到了所有这些,你值得被表扬。

话虽如此,我们很容易被那些吸引人的缩写和“编写简洁代码的十大方法”所吸引,忘记了我们这样做的初衷。如果你不了解目的,以上所有最终都只是徒劳null

无论目的、语言或方法如何,所有软件都必须满足两个标准:

  1. 该软件必须实现其既定目标。

  2. 该软件必须可供未来的开发人员维护(即可读)。

问题在于,要实现第一点,代码只需编译通过、测试通过并按预期运行即可。即使是设计糟糕的代码也能做到这一点。我们花了太多时间专注于第一点,却常常把第二点抛在脑后。充其量,我们只是读几篇关于的文章dev.to(),把所有内容提炼成几条易于遵循的规则,让我们能够自动掌握可维护性。毕竟,让软件自己做事要有趣得多。

但干净、干燥、坚硬的意大利面仍然是意大利面。事实上,它是最糟糕的意大利面!你可能知道我在说什么:你把意大利面煮熟,沥干水分,然后把它放在锅里,天知道要煮多久。它当然是干净的,但等你回想起它的时候,它已经变成了一团无法食用的干硬物。你什么也做不了,只能靠一盘坚硬的意大利面。

同样,一个无法维护的代码库从一开始就注定要失败。新功能很难添加,bug几乎无法修复,新的贡献者会被吓跑。任何被迫维护代码的人都将面临数天甚至数周的痛苦,因为他们需要从这团乱麻中一根一根地找出程序逻辑。

在我们的代码遭遇这种令人不快的命运之前,让我们先来看看我们善意的可维护性原则是如何变得糟糕的。

DRY 遇见荒地

沙漠

DRY:不要重复自己

这个方便的小缩写被广泛使用。它的概念非常明显。与其把同样的代码写十六遍,甚至六遍,不如把它放在一个函数里调用!

然而,必须有一条界线。我见过一些代码库,为了找出一个单一目标函数的功能,我不得不跨多个文件跳转到至少二十几个其他函数和宏。更糟糕的是,我甚至不确定数百个链接文件中的哪一个包含下一个调用堆栈。所有内容都被抽象到了极致,使得代码库完全不可读。

SOLID 的“依赖倒置原则”也存在同样的风险。我们可以将代码抽象到几乎每个函数都只调用其他函数的程度。

到了那个时候,谁都不是赢家。性能会受到影响(听说过“指令缓存未命中”吗?),可读性也会急剧下降。理解代码所需的时间会远远超过预期。

解决这个问题并不简单。DRY 原则很重要,但需要仔细甄别。在抽象某些功能之前,需要考虑成本。然后,你需要仔细确定抽象功能的最佳方法。确保方法易于追踪。

清理犯罪现场

我明白。你想要一个一尘不染的代码库,谁又能责怪你呢?这看起来很舒服,而且会让你看起来像个名副其实的编程天才。然而,在清理代码库的过程中,我们往往会犯一些可怕的错误。

真空着火

评论太少

或许最糟糕的是删除(或根本不添加)意向注释。许多人认为“注释与代码不同步”。一些标准甚至自豪地宣称,注释应该尽量少用,甚至根本不要用。他们劝诫道:“写自注释代码!”

问题在于,任何代码都无法向不熟悉代码的人解释其“为什么”。我坚信并积极践行“注释展现意图”(CSI)的原则。简而言之,每个逻辑块都应该有一条注释来描述其意图,即“为什么”。在这一点上,我们不能完全依赖直觉,因为在编写代码时,几乎所有内容对我们来说都是显而易见的。事实上,没有人能读懂你的想法。

对于那些声称注释不同步的人,我认为只有你允许这种情况发生才会发生。实施像CSI这样的注释标准意味着你将这些注释作为代码审查流程的一部分。如果声明的意图与实际功能不匹配,则应始终将其视为错误并予以解决。在大多数情况下,没有意图注释的代码甚至不应该被允许进入代码库。

请注意,这并不意味着你要一股脑地用重复代码的冗余注释来弄乱你的代码。我发现通常最好先把所有代码都注释掉,然后提交,然后让这些注释搁置几周或几个月。一旦你对代码熟悉了,你就能澄清和/或删除那些冗余的、类似“what”的注释了。

太多糟糕的名字

自文档化的代码真是太棒了。它让我们能够从函数或变量的名称中直接看出其用途。这一点毋庸置疑,我们需要遵循。

但如果事情发展得太过分了怎么办?

RtlWriteDecodedUcsDataIntoSmartLBlobUcsWritingContext();
Enter fullscreen mode Exit fullscreen mode

如果我必须维护这样的功能代码库,我宁愿辞职,成为一名全职音乐家。(如果你有强大的胃,还有更多。)

在描述性名称和可读性名称之间找到平衡。

既然说到这个话题了,如果你在代码中使用了系统匈牙利命名法,那就赶紧停手吧。在做其他事情之前,先用“查找和替换”把那些可怕的、没用的类型前缀删掉。这可不是查尔斯·西蒙尼的本意。(相比之下,应用匈牙利命名法本身就是一个自文档化的命名方法。去查一下就知道了。)

太少了……一切都

其次,干净的代码通常是“简洁”的代码,但在这里容易走极端!虽然你可以用三元运算符将整个条件语句折叠成嵌套的 lambda 表达式,但其他人能读懂吗?

我们常常为了追求美观而牺牲了真正优秀的代码。你可以这样做,并不意味着你应该这样做。务必在简洁性和可读性之间取得良好的平衡。

空白太少

最后,请不要压缩你的工作代码库。永远不要。这样做不干净,只会损害可维护性。事实上,除非代码本身存在特定、客观、可验证的业务场景(例如优化或混淆),否则在任何情况下压缩代码都是个坏主意。迟早会有人需要通读代码来诊断 bug 或复现功能。所以,请保持友善——不要压缩代码。

当 SOLID 变得愚蠢时

撞墙

在面向对象编程中,SOLID 原则非常有用。然而,这些原则都不能盲目应用。

你将要读到的故事是真实的。为了保护那些白痴,名字都已化名。

我曾经在一个相当流行且普及的 Web 平台上工作过。我可以相当肯定地说,这个代码库完全遵循了 SOLID 原则。然而,我也坚持认为,完整的规范源代码应该用来作为无人太阳任务的测试载荷。到目前为止,这是我见过的最糟糕的代码。然而,从 SOLID 原则的角度来看……

  • 单一职责原则:每个类都只有一个特定的职责。而且类的数量非常多,大概有成千上万个。

  • 开放封闭原则:代码库中添加的每个附加功能都以扩展的形式出现,并且始终通过继承实现。是的,始终是继承。

  • 里氏替换原则:每个类都可以被其父类替换,直至替换链的上层,没有例外。

  • 接口隔离原则:你只需要使用你需要的功能。所有功能都基于单继承,所以你不需要因为使用了 Y 而导入 X。

  • 依赖倒置原则:一切都被抽象了。确切地说,一切都被抽象了。它极度依赖数据驱动。

哇,100% 符合 SOLID 标准!然而,这却是一个难以维护的噩梦。每个类最终都继承自同一个家族树,有时甚至深达数百层。要编写一个类,你必须先阅读其他 99 个类。然后,如果在链的上层进行了错误修复或优化(请注意,没有人会费心去实际记录),它会破坏其下方的所有内容。因此,在 3.2 版本中运行良好的代码在 3.3 版本中会完全崩溃。

结果,贡献者们不敢改进继承链上层那些糟糕的代码,担心会毁掉一切。代码还没完成,文档就已经过期了。平台上写了几十本书,出版后就立刻变得毫无用处。

SOLID 是造成这种情况的罪魁祸首吗?绝对不是!这恰恰表明,SOLID 并非某种提高可维护性的灵丹妙药。它的原则必须运用常识和敏锐的洞察力来应用。任何有 OOP 经验的程序员都知道,我所描述的这种继承结构是一种糟糕的设计,但负责这个项目的大型开源团队恰恰犯了同样的错误。他们拥有一个完全 SOLID、完全 DRY 的代码库,然而我认为它堪称计算机历史上最糟糕的生产代码。

TDD:测试驱动灾难

三个臭皮匠做化学实验

说实话,任何能做到 100% 代码覆盖率的测试都是天才。我觉得自己还差得远。虽然我习惯写测试,但我害怕拥抱 TDD 是有原因的:我见过太多代码库因为 TDD 而走上了绝路。

测试的目标应该是检测生产代码中的错误,但代码的目标应该是通过测试!我认为 TDD 特别容易混淆这两个目标。如果你已经编写了测试,你就会本能地开始编写代码来通过测试,而完全忽略了其他方面……比如功能性、可读性和可维护性。你可能会过于专注于单元测试,只是为了把它们标记为“通过”,而没有注意到你的代码正在变成一个糟糕的哈希值,直到为时已晚。

与往常一样,这并不是对 TDD 的抨击,而是对其实践者(以及所有编写测试的人)的警告。为了避免这种狭隘的视野,我建议如下:

在代码编写完成并明显可运行之后,盲写生产测试。 “盲写”的意思是“编写代码时不要查看代码”。在纸上,分解你记得的代码应该做什么,以及不应该做什么。列出清单,并为每个部分编写一个测试。确保每个测试都有明确的通过和失败条件。不要宽容。不要对你的代码做可怕的事情

实际上,这为我带来了两件事:

  1. 在知道我的生产测试尚不存在时,我会专注于编写好的代码,而不是欺骗我预先确定的小守门人。

  2. 我用这种方法发现了很多 bug 和设计缺陷……真的很多。超过一半的致命 bug 和边缘情况都是通过盲测发现的。

如果你全心全意地拥抱 TDD,并且在编写代码之前先编写测试,我建议你更进一步。删除你原来的测试,然后盲目地重写它们。只允许你自己注意测试的功能,而不是测试是如何进行的。

风格重于功能

毫无意义的光剑旋转

代码风格标准固然重要,它能让代码读起来更赏心悦目。然而,即便如此,也难免会走极端。虽然我们应该遵循代码风格标准,但当代码的可读性或可维护性受到制约时,我们也要做好打破这些标准的准备。以下是一些示例及其蕴含的原则:

  • 在 80 或 120 个字符处中断是否会使该行更难解析?(是的,这种情况很少见,但确实会发生。)这样做可以提高可读性和/或可维护性,因此请允许自己使用样式例外。

  • 喜欢单行、无括号的条件语句吗?编程错误也一样。谨慎使用时髦的快捷方式。有时括号更安全。

  • 你是否需要修改代码才能保持部分代码风格?(是的,这种情况确实会发生!)别再这样做了! 仅仅为了保持代码风格,你永远不应该对代码进行功能性的修改。

  • 您是否正在反对三元条件语句?相反,您是否喜欢它们,因为它们让您看起来像个 ALPHA 黑客?请使用兼具可读性、可维护性和功能性的工具。请将您的意见放在一边。

不包含常识

有很多非常有用的原则和标准可以帮助我们编写优秀的代码……但不要轻信那些炒作!它们都不是编写可维护代码的唯一正确答案。你始终需要积极投入到编写优秀代码的过程中。

在编程中,意大利面条已经够糟糕了,但结实、干燥、干净的意大利面条呢?这才是最难解开的!

文章来源:https://dev.to/codemouse92/clean-dry-solid-spaghetti-1lgm
PREV
极简 Python:类 类在会话中 声明方法 类与静态方法 初始化器和构造函数 变量作用域:私有和公共属性 继承 保留类!复习
NEXT
追踪你的进步来提高你的信心