Code Smell 是代码中的一个表面迹象,预示着应用程序设计或架构中的潜在问题。该术语由 Kent Beck 提出,并由 Martin Fowler 在《Refactoring: Improving the Design of Existing Code》一书中推广。根据 Martin Fowler 的说法,代码坏味不一定意味着错误,但几乎总是表明需要重构以提高可维护性。
要点
Code Smell(代码坏味)— 是对源代码中很可能表明更深层次问题的症状的一种比喻。该术语本身没有正式的定义 — 它是一种基于程序员经验的启发式方法。Martin Fowler 和 Kent Beck 于 1999 年在《Refactoring》一书中首次系统化了 22 种坏味,并且大多数在几十年后仍然具有现实意义。
理解 Code Smell 和错误之间的区别很重要。坏味不是错误:代码可以编译、运行并产生正确的结果。问题在于这种代码难以阅读、修改和测试。随着时间的推移,每次更改的成本都会增加,而对重构正确性的信心则会下降。静态分析工具(SonarQube、Detekt、SwiftLint)可以自动检测许多坏味。
Code Smell 的启发式特性意味着并非每个长方法都需要拆分,也并非每个大类都需要重构。决定由程序员做出,评估上下文:更改频率、模块的关键性、开发计划。经验丰富的工程师会凭直觉闻到坏味 — 代码「闻起来不舒服」,尽管形式上所有规则都得到了遵守。
Fowler 区分了 22 种坏味,它们分为几个类别。对于移动开发,最相关的是结构性坏味、面向对象设计坏味以及与平台限制相关的特定问题。让我们用实际案例中的例子来研究每个组。
Long Method(长方法)— 移动应用中最常见的坏味。带有注册表单的屏幕通常包含一个 200+ 行的 setupUI 方法,它创建所有 View、设置约束、订阅事件并处理错误。解决方案:按逻辑块拆分为方法 — configureEmailField、configurePasswordField、setupConstraints、bindViewModel。
Large Class(大类)— 负责显示、导航、业务逻辑和网络通信的 Activity 或 ViewController。这样的类违反了单一职责原则,并包含数十个字段和方法。在 Android 中,这通常是一个包含不同屏幕逻辑的 1000+ 行的 Fragment。解决方案:分离 presenter/ViewModel,将网络工作转移到 repository,将导航转移到协调器。
Duplicate Code(代码重复)— 在应用程序的不同部分复制相同的块。典型示例:在目录和收藏夹中显示产品卡片的两个屏幕。如果显示逻辑被复制,在一个地方修复错误不会在另一个地方修复它。解决方案:将通用逻辑转移到可重用组件或扩展中。
Feature Envy(嫉妒其他类)— 一个类的方法密集地使用另一个类的数据。在 Android 中,这表现为 ViewModel 直接访问 User 模型的字段,而不是调用模型的方法。信号:如果可以将方法移动到其数据被使用的类中 — 请移动它。Switch Statements(条件链)— 检查对象类型的 switch 结构或 if-else 链。应该使用多态性或策略模式代替。
Data Class — 只存储数据但不包含行为的类。数据类(在 Kotlin 中)或结构体(在 Swift 中)本身并不是坏味。当处理这些数据的业务逻辑分散在整个代码库中而不是被封裝时,问题就出现了。Refused Bequest — 继承者不使用父类的大部分方法,并用空实现覆盖它们。不正确继承的标志:用组合替换继承。
God Activity / God Fragment — 了解一切的 Activity 或 Fragment:生命周期、数据、导航、权限、DI。这是应用程序中维护成本最高的类。解决方案:MVVM、MVI 或 Clean Architecture 等架构模式分担了责任。Giant ViewController — iOS 的类似物,其中 UIViewController 包含屏幕的所有逻辑,通常超过 500 行。
Hardcoded Resources — 直接嵌入代码中的字符串、颜色、尺寸、API URL。在 Android 中,这违反了 R 资源系统的使用,在 iOS 中 — 违反了 NSLocalizedString 和 Asset Catalog。修复:将所有字符串移动到 strings.xml 或 Localizable.strings,URL 移动到配置文件,尺寸移动到 dimens。Leaking Context — 对 Activity 或 ViewController 的引用保留时间超过组件本身的寿命。导致内存泄漏和崩溃。解决方案:弱引用、Jetpack Lifecycle、RxSwift DisposeBag。
| 坏味 | 出现位置 | 解决方案 |
|---|---|---|
| Long Method | Android/iOS | Extract Method、拆分 |
| Large Class | Activity、ViewController | MVVM、VIPER、Clean Arch |
| Duplicate Code | 任何屏幕 | Shared Component、DRY |
| Feature Envy | ViewModel、Presenter | Move Method |
| Leaking Context | Android | 生命周期感知组件 |
Code review — 检测坏味最可靠的方法。人眼能注意到自动分析器遗漏的不自然结构。如果团队使用典型坏味的检查清单,代码审查的效率会提高。建议在一次会话中检查不超过 200–400 行代码 — 超过此阈值后,注意力会下降,坏味开始被遗漏。
静态分析 自动化了结构性坏味的搜索。对于 Android,标准工具是 Detekt(Kotlin)和 Android Lint,对于 iOS — SwiftLint 和 SonarQube。这些工具可以找到长方法、大类、代码重复和许多其他问题。根据项目配置规则很重要 — 默认配置通常过于严格,或者相反,会遗漏关键的坏味。
代码度量 提供了客观标准:Cyclomatic Complexity(阈值 >10 需要注意)、Lines of Code per Method(阈值 >30)、Depth of Inheritance(>3 — 值得深思)。CodeMetrics(Xcode)和 Gradle Metrics Plugin 等工具构建度量随时间变化的图表。如果方法的复杂度在最近一次提交后从 5 增加到 15 — 这是重构的信号。
// 示例:Cyclomatic 复杂度 = 7 的方法(高于阈值 5)
fun processOrder(order: Order) {
if (order.status == Status.NEW) { /* 10 行 */ }
else if (order.status == Status.PAID) { /* 15 行 */ }
else if (order.status == Status.SHIPPED) { /* 20 行 */ }
else if (order.status == Status.DELIVERED) { /* 8 行 */ }
else if (order.status == Status.CANCELLED) { /* 5 行 */ }
else { throw IllegalStateException() }
}
// 修复:用多态性替代 switch
interface OrderHandler {
fun handle(order: Order)
}
自动 搜索坏味不能取代代码审查:静态分析器只能找到结构性问题,但无法捕获语义坏味(Feature Envy、Inappropriate Intimacy)。自动工具和人工控制的结合提供了最好的结果。配置 CI/CD 流水线,使其在超过复杂度或方法长度阈值时构建失败。
重构 — 消除代码坏味的主要方法。Fowler 描述了数十种重构技术,每种技术都适用于特定的坏味。Extract Method — 用于长方法,Extract Class — 用于大类,Move Method — 用于 Feature Envy。以小步骤进行重构很重要,每次更改后要保持代码的功能。
重构前进行测试 — 强制性条件。如果代码没有被单元测试覆盖,重构就会变成结果未知的重写。对于没有测试的遗留代码,使用 Characterisation Tests — 编写记录当前行为的测试,然后进行重构。测试可以确保重构后业务逻辑没有损坏。
循序渐进 — 在移动开发中成功消除坏味的关键。不要试图完全重写 God Activity。首先分离导航层,然后是数据层,接着是显示逻辑。每一步都要伴随提交和运行测试。使用功能开关为部分用户启用重构,并在出现问题时回滚。
IDE 工具 自动化了许多重构技术。Android Studio 和 IntelliJ IDEA 提供内置重构:Extract Method、Extract Interface、Pull Members Up、Encapsulate Fields。Xcode(从版本 14 开始)改进了对 Swift 重构的支持。使用自动重构与手动复制代码相比降低了错误风险。
移动开发 增加了与平台限制相关的自身特定坏味。在 Android 中,这些是 Context 泄漏、未关闭的 Cursor、Lifecycle 的不正确使用。在 iOS 中 — 通过闭包产生的 retain cycle、与 Auto Layout 的错误使用、巨大的 ViewController。这些坏味不仅会降低可维护性,还会直接影响应用程序的性能和稳定性。
Callback Hell — 使用异步操作的代码的典型坏味。嵌套的回调(callback inside callback)使代码难以阅读和调试。解决方案:协程(Kotlin)、async/await(Swift 5.5+)、RxJava/RxSwift 或 Combine。根据 Google I/O 2023 的数据,从回调风格迁移到协程的项目将错误数量减少了 30%,并加快了对新功能的添加。
Platform Coupling — 业务逻辑与平台组件的紧密耦合。测试这样的逻辑需要启动模拟器,这会减慢反馈循环。修复:Clean Architecture 将代码分为 Domain 层(纯 Kotlin/Swift,无平台依赖)和 Data/UI 层(带有平台依赖)。业务逻辑在没有模拟器的 JVM 上进行测试。
常见问题
不是 — Code Smell 不是错误。带有坏味的代码可以正常工作,但难以维护、修改和测试。错误是不正确的行为,坏味是对未来潜在问题的警告。
22 种坏味 在《Refactoring》第二版(2019 年)中。其中包括 Long Method、Large Class、Primitive Obsession、Data Clumps、Switch Statements、Speculative Generality 等。社区为现代范式和平台添加了数十种新的坏味。
组合 提供了最佳结果:Detekt(Android/Kotlin)、SwiftLint(iOS)、SonarQube(两者)用于自动分析,代码审查用于语义坏味。没有工具能发现 100% 的问题 — 人的经验仍然至关重要。
可以,如果代码很少更改或在不久的将来会被完全重写。然而,坏味的积累会变成技术债务:每次新的更改都变得更加困难,修复成本呈指数级增长。
有 — 声明式框架创造了新的坏味:巨大的 @State 块、与重复渲染的错误使用、过度重组、未能提取到单独的 View。对于 SwiftUI,典型的坏味是带有数十个 @State 变量的 Massive View。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。