坏味道名称 (中文/英文)
坏味道的解释
建议的重构手法
Kotlin 示例
神秘命名 (Mysterious Name)
好的命名是整洁编程的核心。如果函数名令人迷惑,或变量名起得不好,会使代码难以理解,并且下次阅读代码时需要重新思考意图。
使用 改变函数声明 或 变量改名。
错误示例: fun calc(d: Date, t: Int) : String 改进: fun formatDeliveryTime(requestDate: Date, deliveryHours: Int) : String
重复代码 (Duplicated Code)
在一个以上的地点看到相同的代码结构。这会增加阅读时的仔细程度,一旦需要修改,必须找出所有的副本并同步修改,这是优秀设计的大敌。
采用 提炼函数。如果代码相似但非完全相同,可先用 移动语句 重组。对于子类中的重复,使用 函数上移。
两个不同的 Fragment 中有完全相同的代码块用于初始化 RecyclerView 的布局管理器和适配器。
过长函数 (Long Function)
函数越长,就越难理解。小函数能提供更好的阐释力、更易于分享,是好程序的核心。关键在于函数“做什么”和“如何做”之间的语义距离。
提炼函数 是主要的解决方案。使用 以查询取代临时变量 消除局部变量的障碍。
一个 Android ViewModel 中的 loadUserProfile() 函数超过 80 行,处理了网络请求、数据转换、本地缓存和错误处理等多个不相关的逻辑。
过长参数列表 (Long Parameter List)
长的参数列表本身经常令人迷惑。参数列表阐述了函数如何与外部世界共处,过长会增加耦合度。
使用 引入参数对象 将总是同时出现的参数合并成一个对象。使用 保持对象完整 传入整个数据结构。
fun setupUI(width: Int, height: Int, color: Int, title: String, isLarge: Boolean, isDarkMode: Boolean)
全局数据 (Global Data)
它是 刺鼻的坏味道之一。代码库任何角落都可以修改它,且难以探测修改的来源,极易导致诡异的 Bug。
首先使用 封装变量 ,将其包装在函数中。随后,将函数和封装的数据搬移到类或模块中,以控制其作用域。
Kotlin object 声明的一个全局 Configuration 单例,允许在应用任何地方直接通过其公有属性修改配置值。
可变数据 (Mutable Data)
对数据的修改常导致出乎意料的结果和难以发现的 Bug。
使用 封装变量 限制数据更新通过少数函数进行。使用 拆分变量 避免一个变量在不同时段存储不同的东西。如果可计算,使用 以查询取代派生变量 消除不必要的累积变量。
一个 OrderProcessor 类中的 total 字段,先被赋值为商品价格,随后在同一函数中被加上运费,再被减去折扣。
发散式变化 (Divergent Change)
某个模块经常因为 不同的原因 在 不同方向 上发生变化。修改一个方面(如数据库)需要修改该模块,修改另一个方面(如业务规则)也需要修改该模块。
使用 拆分阶段 或 提炼类 将不同的上下文分离到独立的模块中。
一个 Repository 类既处理数据存储逻辑(数据库变化需要修改),又处理数据权限过滤逻辑(权限规则变化需要修改)。
霰弹式修改 (Shotgun Surgery)
每遇到某种变化,你都必须在 许多不同的类内 做出许多小修改。代码散布四处,难以找到。
使用 搬移函数 和 搬移字段 将所有需要修改的代码集中到一个模块。
如果要修改用户状态,需要在 User 类中更新状态,在 NotificationService 中添加日志,并在 AnalyticsTracker 中发送事件,涉及三个不同类的小修改。
依恋情结 (Feature Envy)
一个函数对 其他模块 中的数据和行为的兴趣,比对其自身宿主模块的兴趣更高。函数应该和它所使用的数据待在一起。
使用 搬移函数 将函数搬移到它最需要的数据所在的模块。
PaymentService.calculateTaxes(Order order) 函数使用了 90% 的 order 字段,而只使用了 10% 的 PaymentService 内部数据。
数据泥团 (Data Clumps)
相同的三四项数据(例如:城市、街道、邮编)总是成群结队地出现在不同的地方,包括作为字段和函数签名中的参数。
运用 提炼类 将它们提炼到独立对象中。运用 引入参数对象 简化函数签名。
(addressLine1: String, addressLine2: String, city: String, zipCode: String) 这一组参数在多个函数签名中重复出现。
基本类型偏执 (Primitive Obsession)
大量使用基本类型(如整数、字符串)来表示概念,而非创建对问题域有用的基本类型(如 Money, PhoneNumber 等)。
运用 以对象取代基本类型 将数据值替换为对象。
使用 String 字段来存储颜色代码(如 "#FFFFFF"),而不是创建 Color 值对象,以封装颜色验证或格式转换逻辑。
重复的 switch (Repeated Switches)
在不同的地方反复使用 同样的 switch/when 逻辑 来根据某个条件进行分支选择。每增加一个选择分支,必须找到所有 when 块并逐一更新。
使用 以多态取代条件表达式 来消除重复的条件逻辑。
在 Android 权限管理中,PermissionChecker 和 PermissionRequester 都包含一个基于权限类型 when (permissionType) 的逻辑块,处理不同权限的逻辑。
循环语句 (Loops)
传统的循环语句,不如管道操作(如 filter 和 map)能更快地看清被处理的元素和处理动作。
使用 以管道取代循环 ,用集合操作替代传统的 for 或 while 循环。
使用 for 循环手动迭代列表来计算总和,而不是使用 list.sumOf { ... }。
冗赘的元素 (Lazy Element)
程序元素(如类和函数)没有带来足够的结构或价值。例如,函数名与实现代码同样清晰,或者类只剩下了一个简单的委托函数。
使用 内联函数 或 内联类 。如果处于继承体系,使用 折叠继承体系 。
一个 OrderHelper 类只有一个 calculateTotal() 方法,该方法可以直接被调用方访问。
夸夸其谈通用性 (Speculative Generality)
程序员添加了不必要的灵活性机制(如钩子、参数),认为“总有一天需要做这事”,结果造成系统更难理解和维护。
使用 折叠继承体系 移除无用的抽象类。使用 内联函数 和 内联类 移除不必要的间接层。
定义了一个包含十几个可选参数的 GenericBuilder,用于构造一个只有两个常用参数的对象。
临时字段 (Temporary Field)
类中内部某个字段仅为某种特定情况(例如,执行某个复杂算法的临时存储)而设。
使用 提炼类 给该字段创造一个家。使用 搬移函数 将相关代码搬进新家。
在 Invoice 类中,_currentTaxRate 字段仅在执行 calculateFinalAmount() 算法期间使用和赋值。
过长的消息链 (Message Chains)
用户向一个对象请求另一个对象,然后再向后者请求另一个对象,形成一长串访问器链。客户端代码将与查找过程中的导航结构紧密耦合。
使用 隐藏委托关系 。将使用最终对象的代码提炼出来,并用 搬移函数 推入消息链。
order.customer.address.city。客户端需要了解 customer 如何持有 address。
中间人 (Middle Man)
类的接口有一半的函数都委托给其他类,造成过度运用委托。客户端被迫通过这些不干实事的函数访问真正负责的对象。
使用 移除中间人,直接和真正负责的对象打交道。
UserController 上有一个 getUserName() 方法,它只是简单调用了 User 实体上的 getName()。
内幕交易 (Insider Trading)
模块之间大量交换数据,即“窃窃私语”,导致模块间耦合增加。
使用 搬移函数 和 搬移字段 减少私下交流。
BillingModule 直接读取并修改 UserModule 的私有计费相关的配置属性。
过大的类 (Large Class)
一个类承担了太多责任,包含太多字段和代码,难以理解、重复代码多。
运用 提炼类 将彼此相关的变量和行为提炼到新类中。
一个 MainActivity 承担了数据获取、权限请求、视图绑定、多个子视图的逻辑以及用户认证状态管理。
异曲同工的类 (Alternative Classes with Different Interfaces)
两个类做类似的事情,但接口不一致,使得它们无法互换。
使用 改变函数声明 使函数签名一致。使用 搬移函数 使行为一致。
两个用于存储日志的类,一个使用 storeLog(),另一个使用 saveMessage(),虽然功能相同但名称不统一。
纯数据类 (Data Class)
它们只拥有字段和访问函数,除此之外一无长物。行为被放在了错误的地方,即被其他类过分细琐地操控着。
运用 移除设值函数 保护不应被修改的字段。尝试以 搬移函数 把处理数据的行为搬移到纯数据类里。
Kotlin data class User(val firstName: String, val lastName: String, val dob: Date),但所有用户年龄计算、姓名格式化逻辑都在外部服务中。
被拒绝的遗赠 (Refused Bequest)
子类继承了超类,但它不想或不需要继承超类提供的所有函数和数据。这表明继承体系设计可能存在错误。
使用 函数下移 和 字段下移 将用不到的函数推下给其他类。如果拒绝支持超类接口,应使用 以委托取代子类。
OfflineCacheManager 继承了 BaseCacheManager,但它覆写了 clearCache() 使其不做任何操作(即拒绝了清空缓存的行为)。
注释 (Comments)
注释本身是香味,但它们常被当作 “除臭剂” 来使用。如果注释的存在是因为代码很糟糕(比如需要长注释来解释复杂的逻辑),它就指出了重构的机会。
尝试重构,试着让所有注释都变得多余。如果需要解释代码做了什么,试试 提炼函数。
// 检查用户是否已登录,并且其权限级别高于最低限制,且不是测试账号 (这段长注释提示应将复杂的布尔逻辑提炼为一个名为 isAuthorizedForAction() 的函数)。
 
学习笔记——《重构改善现有代码2》:构造测试体系学习笔记——《重构改善现有代码2》:重构的挑战