坏味道名称 (中文/英文) | 坏味道的解释 | 建议的重构手法 | 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() 的函数)。 |
- Author:CoderWdd
- URL:https://www.wuinsights.top//article/a1697a6e-df79-4d1b-8b4c-23bf1375e46d
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts