“代码坏味”是程序员使用的一个非正式术语,用于描述糟糕代码中常见的软件设计模式。最好将代码坏味理解为代码质量低下的警告信号,而非将其视为错误或缺陷本身。
“代码坏味”一词由 Kent Beck 创造,并在 1999 年他与 Martin Fowler 合著的里程碑式著作《重构:改善既有代码的设计》——具体而言,是在题为“代码中的坏味”的章节——中流行起来。Fowler 本人将代码坏味简明地表述为“一种通常对应系统中更深层问题的表面迹象”。1
与真正的软件错误不同,代码坏味不会阻止源代码编译、运行并执行其预期功能——它的存在也不一定总是指示实际存在的问题。想象走进一个家,闻到某种发霉或难闻的气味。这股气味可能来自冰箱里过期的食物或需要拿出去的垃圾;更严重的话,可能来自墙壁里的霉菌或正在腐烂的东西——但也可能只是来自一些气味浓烈但完全可以安全食用的奶酪。无论如何,这股气味都需要进一步检查。
对于经验丰富的软件工程师来说,代码中的某些模式会感觉“不对劲”。久而久之,人们可能会本能地将它们的存在与某些复杂问题、低效情况或未来的隐患联系起来。为特定的代码坏味命名并加以普及,有助于熟悉这些适得其反的模式(或称“反模式”)及其违反的设计原则。这种熟悉程度,反过来又能让这些反模式更有可能在代码审查中被发现和纠正。正如 Fowler 所说,“坏味,顾名思义,就是能够快速发现——或者说能够嗅探到的东西。”
代码坏味如果不加以处理,是导致技术债务的主要因素。一项研究指出,75% 的代码审查缺陷虽然不影响程序运行,但确实“影响软件的可演进性”。2即使某个反模式或不良习惯今天没有引发错误,它也会显著增加代码库日后出现错误、崩溃或安全漏洞的风险。至少,已知代码坏味的存在会降低代码的可读性和可维护性,使其更难理解、更新和改进。
通过 Think 时事通讯,了解有关 AI、自动化、数据等方面最重要且最有趣的行业趋势。请参阅 IBM 隐私声明。
尽管许多常见的代码坏味仍以 Beck 和 Fowler 在《重构》一书中创造的名称广为人知,但不存在一份唯一权威、普遍认可的代码坏味列表。但某个代码坏味是应该用这个名称还是另一个名称来指代,或者应该被归类为独立的反模式而非另一个反模式的子类型,在很大程度上并不重要。重要的是,代码坏味的名称和描述是否足够清晰、直观,能够帮助软件开发团队分享和贯彻有效的设计原则。
同样,对于不同类别的代码坏味,也没有普遍认可的分类法。本文主要借鉴了 Jerzyk 和 Madeyski 在 2023 年提出的分类法。3他们的研究成果发布在 codesmells.org 维护的实用目录中,该目录为每种坏味提供了额外的背景、代码示例及合适的重构技巧。
Jerzyk 和 Madeyski 指出,最常被引用的代码坏味分类法遵循 Mäntylä 和 Lassenius 在 2006 年提出的五个分组,他们将 Fowler 和 Beck 引入的 22 种代码坏味(以及他们自己新增的一种)划分为五个不同的类别:臃肿代码、面向对象滥用、变更阻碍、冗余代码和耦合代码。4在对正式发表的文献和“灰色材料”(如博客、论坛和维基)尝试了详尽的审查之后,Jerzyk 和 Madeyski 将 56 种不同的代码坏味编目为 9 个分组(其中包括本段前面提到的那 5 个)。
虽然本节主要遵循他们提出的 9 类结构,但必须注意,这类分组是非正式且主观的:最有用的分类法是你认为最直观的那个。理解代码坏味可能引入的各种复杂问题或低效情况,比死记硬背单个坏味更能提供有意义的概念认知。正如 Mäntylä 所说,“面对一长串平铺的代码坏味列表,很容易只见树木不见森林。”5
臃肿代码是指常常导致方法、类或代码块变得过于庞大而难以驾驭的代码坏味。这会降低可读性,并使代码更难维护或修改。
臃肿代码的例子包括:
数据簇:经常到处一起出现的变量组。
过大的类:试图做太多事情、包含太多变量、缺乏内聚性的类。
过长函数(长方法):包含太多行代码的方法。
过长参数列表:需要太多参数才能正确运行的函数。
基本类型偏执:使用基本数据类型而不是专门的小对象。
顾名思义,变更阻碍指的是阻碍修改、添加或以其他方式进一步开发软件的能力的代码坏味。一个典型的变更阻碍实例是,某种代码结构迫使你在多个地方进行一系列编辑,仅仅为了实现一处简单的修改。
这类代码坏味违反了 Robert C. Martin 的单一职责原则 (SRP),该原则主张对任何一个代码模块的更改应当只能源自一处。正如 Martin 在一篇关于 SRP 的博客文章所说:“将因相同原因而变化的事物聚合到一起。将因不同原因而变化的事物分离开来。”6
变更阻碍的例子包括:
霰弹式修改:实现一处变更需要同时修改许多分散的模块(本质上是“大类”的反面)。
发散式变化:类似于霰弹式修改的反面,实现一处变更需要在单个类中进行许多修改。
回调地狱:一种代码结构,其中许多方法通过大量的缩进和花括号彼此深度嵌套,模糊了因果关系,使代码难以阅读和维护。
数据贩子通过比实际需要更多的类或函数来传递数据。这会造成不必要的依赖和复杂性,常常导致代码元素仅仅持有或分发数据,却未提供任何有意义的行为。
数据贩子的例子包括:
中间人:唯一的工作就是向其他对象委托的类。
流浪数据:数据“搭便车”穿过一连串根本用不到它的方法。
全局数据:变量可以在代码库的任何地方被修改,当出现问题时,代码库中的每个函数都成了嫌疑对象。
直观地说,冗余代码就是可有可无的代码元素:移除它们会让代码库更简洁、更易读,且对整体功能没有实质影响。
冗余代码的例子包括:
注释:虽然注释通常是好事,但有时它们被用作代码坏味的“除臭剂”,解释代码而不是改进代码。例如,如果注释描述的是某段代码正在做什么,那么它被用来掩盖本身不够直观或可读性不够的代码。某些注释也会随时间推移变得冗余或过时。更具体的注释行为在其他代码坏味中也有体现。
数据类:仅包含字段和访问器、缺乏有意义行为的类。
死代码:由于重构和其他修改而变得过时,不再被执行的代码元素。
重复代码:在多处存在的完全相同或非常相似的代码结构。
惰性类(又称惰性元素):存在意义过小的类或函数。
臆想性通用代码:为支持假想中的未来功能而添加的多余代码。
词汇滥用用源于糟糕的命名约定、不一致的格式或晦涩的语法。简而言之,就是代码的用词无法直观地匹配相应的代码行为,从而妨碍可读性。
词汇滥用的例子包括:
魔法数字:在代码中插入的、未经充分解释或缺乏上下文的未命名数字。
谬误注释:由于周围代码已变更而不再准确的注释。因为注释实际上不会被执行,它们常常逃过 lint 工具和其他自动化检查。
神秘命名:命名不当的函数或变量,隐藏了其真实意图。
谬误方法名:基于常见的约定和期望,其名称具有主动误导性的函数。例如,一个名为 getSomething 的函数实际上不返回任何东西。
混淆代码是指那些用不必要的曲折、复杂或“聪明”的方式编写,将代码的底层意图掩埋在不必要的抽象之下的代码元素。这些代码坏味使源代码读起来混乱,因此让未来的程序员在需要时难以理解和修改它。
混淆代码的例子包括:
垂直分隔:相关的、有关联的代码元素之间不必要的大距离,例如一个变量在方法顶部声明,直到 50 行之后才被使用。
意图模糊:这是更广泛的一类函数、变量、名称和数字,其目的既不直观,也无法在上下文中清晰呈现。
复杂的布尔表达式:令人费解的逻辑流程,例如包含双重否定或
聪明代码:能正常工作的代码,但在存在广为人知的内置方法及其他常规解决方案的情况下,却用难以理解的自定义语言来替代。
面向对象滥用(或称面向对象滥用)是指未能完全或正确应用面向对象设计原则的代码坏味。例如,switch 语句在过程式编程中很有用,但在面向对象编程中应避免使用。
面向对象滥用的例子包括:
接口不同的替代类:执行相似功能但使用完全不同的方法名的类。
拒绝遗赠:不需要或不使用继承方法的子类。
switch 语句(又称条件复杂性,又称重复开关):完全相同的 switch 语句在代码库中多处重复出现。
临时字段:在经常不需要的地方创建的变量,通常仅在特定情况下使用。
理解代码坏味的主要好处在于,识别它们能实现更有效的重构:即在不修改外部行为或功能的前提下更新源代码的实践。通过重构定期清理代码,对于减少技术债务以及促进代码库随着时间的推移进行快速、有效的改进和补充至关重要。
重构在很大程度上可以理解为识别和处理代码坏味的过程。其目标是在问题对功能产生负面影响之前将其修复——到了那时,这个过程就更像是调试而非重构了。
现代智能体式工程平台,如 IBM Bob,通常能实时提供自动化的重构建议。通过对实际代码库上下文中的常见代码坏味进行广泛训练,而非死记硬背代码坏味的定义,此类平台的 AI 代码重构能力使开发者能够扩大产出,而不会相应地增加那些日后难以理解和修改的笨拙 AI 生成代码。
借助您的 AI 合作伙伴 IBM® Bob,加速软件交付,实现安全的意图感知型开发。
利用企业级工具更快地开发、部署和管理 AI 应用程序。
用智能 AI 现代化重新构想旧版系统。