代码异味

代码膨胀

指的是代码、方法或类变得过于庞大,难以维护。这些问题通常不会立即出现,而是随着项目的演进逐渐积累,特别是在没有人主动优化它们的情况下。

  1. 过长的方法

    方法的代码行数过多,通常来说,如果一个方法超过 10 行,就应该开始考虑是否需要拆分或优化。

  2. 过大的类

    一个类包含了太多的字段、方法或者代码行数,导致类的职责不清晰,难以理解和维护。

  3. 原始类型偏执

    过度使用基本数据类型,而不是封装成更具体的小对象。例如:

    • 直接使用整数、字符串来表示复杂概念(如货币、范围、电话号码等),而不是定义专门的类。
    • 使用常量来表示特定的信息,比如 const int USER_ADMIN_ROLE = 1 代表管理员权限,而不是使用枚举或封装类。
    • 在数据结构(如数组或哈希表)中使用字符串常量作为字段名称,而不是使用对象封装这些数据。
  4. 过长的参数列表

    方法的参数超过 3~4 个,可能导致调用复杂、可读性下降。通常可以使用对象封装参数,或者利用构造函数和 setter 方法逐步构建参数。

  5. 数据堆积

    代码的不同部分包含了相同的一组变量(例如数据库连接参数经常一起出现)。这种情况通常意味着这些数据应该被封装到一个独立的类中,以减少重复代码并提高可维护性。

面向对象滥用

这些代码问题通常是由于不完整或不正确地应用面向对象编程(OOP)原则导致的。

  1. switch 语句(Switch Statements)

    代码中存在复杂的 switch 语句大量的 if-else 语句

    这通常表明代码没有充分利用多态,而是基于条件判断执行不同逻辑,而这些逻辑本可以通过继承和重写方法来实现。

    解决方案通常是使用策略模式、状态模式或工厂模式来代替 switch 语句。

  2. 临时字段(Temporary Field)

    类的某些字段只在特定情况下才被赋值,在其他情况下这些字段是空的或无意义的

    这可能意味着:

    • 这些字段应该被移入一个单独的类(如一个封装上下文的类)。
    • 这些字段应该在需要的时候才创建,避免无意义的实例变量。
    • 可能需要重构方法,减少不必要的字段依赖
  3. 拒绝继承(Refused Bequest)

    子类只使用了父类的部分方法或属性,导致继承层次结构不合理。

    可能的后果:

    • 子类要么不使用父类的方法,造成代码浪费。
    • 子类要么重写父类方法并抛出异常,违背了里氏替换原则(Liskov Substitution Principle, LSP)

    解决方案:

    • 重新评估继承关系,看看是否可以改用组合而非继承
    • 拆分父类,只让子类继承它真正需要的部分。
  4. 不同接口但功能相似的类(Alternative Classes with Different Interfaces)

    两个类的功能基本相同,但它们的方法名称不同,导致使用它们的代码缺乏统一性

    这通常是因为不同团队或不同时间开发时没有统一约定,导致多个类做着类似的事情但接口不同。

    解决方案:

    • 重构这两个类,合并成一个类,或者让它们实现同一个接口
    • 提供适配器模式(Adapter Pattern),让它们可以通过相同的方式被调用。

变更阻碍者

这些代码问题意味着,如果你需要修改某个代码的某一部分,就不得不在许多地方做出相应的修改,导致开发变得更加复杂和昂贵。

  1. 多向变更(Divergent Change)

    修改一个类时,需要修改许多不相关的方法

    例如:如果你添加一个新产品类型,不仅要修改查询方法,还要修改显示方法、下单方法等,说明这个类承担了过多的职责

    解决方案:

    • 拆分类,将不同的功能放入各自独立的类中。
    • 使用策略模式(Strategy Pattern),将变化的逻辑封装到独立的类中,减少对核心类的修改。
  2. 散弹式修改(Shotgun Surgery)

    修改某个功能时,需要在多个类中进行多处小修改,例如:你需要修改数据库字段名称,但不仅要改数据库代码,还得改多个业务逻辑类、UI 代码等。

    这种情况通常表明某个功能被过度分散,导致修改变得复杂。

    解决方案:

    • 将相关逻辑集中到单个类或模块中,减少对多个类的依赖。
    • 使用封装(Encapsulation),例如提供公共 API,避免直接访问多个类的内部数据。
  3. 平行继承体系(Parallel Inheritance Hierarchies)

    每次为某个类创建一个子类时,都必须为另一个类创建一个对应的子类

    例如:你有一个 Product 类,每次你创建一个新的 DigitalProduct 子类,你也不得不为 ProductManager 创建 DigitalProductManager,导致继承结构成对增长

    问题:这种做法会导致继承体系过于复杂,修改某个类时,另一个类也必须跟着修改,降低了灵活性。

    解决方案:

    • 使用组合代替继承(优先考虑“组合优于继承”原则)。
    • 使用抽象工厂(Abstract Factory)来管理对象的创建,避免手动扩展多个平行的类层次。

冗余代码

冗余代码指的是不必要的、不起作用的代码,如果删除它们,代码会更加清晰、简洁且易于维护

  1. 过多注释(Comments)

    方法里充满了解释性的注释,可能是因为代码难以理解,或者过于冗长

    问题:好的代码应该自解释,如果代码需要大量注释才能理解,说明可能存在命名不清晰逻辑过于复杂的问题。

    解决方案:

    • 使用清晰的变量名、方法名,让代码本身表达意图,而不是依赖注释。
    • 重构代码,拆分复杂的方法,减少对注释的依赖。
  2. 重复代码(Duplicate Code)

    两个代码片段几乎相同,可能是:

    • 两个方法内部逻辑类似
    • 多个类有类似的方法或代码块

    问题:代码冗余,修改时需要同步修改多个地方,容易出错

    解决方案:

    • 提取公共方法,减少重复代码。
    • 使用继承或组合,将相同逻辑集中到基类或独立模块中。
  3. 懒惰类(Lazy Class)

    一个类存在但几乎没有作用,比如:

    • 这个类原本是为了扩展而创建的,但后来并没有增加新的功能。
    • 代码结构调整后,这个类变得不再有实际用途

    解决方案:

    • 合并类,把它的功能整合到相关的类中。
    • 删除类,如果它不再有实际作用。
  4. 数据类(Data Class)

    这个类只有字段和对应的 getter/setter,但不包含任何逻辑,仅作为数据存储

    问题:这种类缺乏封装,所有逻辑都在其他类中,导致代码分散

    解决方案:

    • 将数据操作方法放入数据类,让它能独立处理自己的数据(遵循面向对象的封装原则)。
    • 使用领域对象(Domain Objects),让数据类不仅仅存储数据,还能执行相关的业务逻辑
  5. 死代码(Dead Code)

    变量、参数、字段、方法或类已经不再被使用,通常是由于:

    • 代码重构后忘记删除旧代码
    • 曾经用于某个功能,但功能被废弃了

    问题:

    • 代码越来越庞大,增加维护难度。
    • 影响阅读,让开发者误以为某些代码仍然重要。

    解决方案:

    • 定期清理代码,删除不再使用的变量、方法或类。
    • 使用静态代码分析工具(如 Clang-Tidy、SonarQube)来检测死代码。
  6. 过度设计(Speculative Generality)

    代码包含未被使用的类、方法、字段或参数,通常是因为开发者为了“未来可能用到”而提前设计

    问题:

    • 过度设计增加了代码复杂度,但这些功能实际上从未被使用
    • 维护这样的代码浪费时间和精力

    解决方案:

    • 遵循 YAGNI 原则(You Ain't Gonna Need It),不要提前实现不确定的功能。
    • 删除未使用的代码,如果未来确实需要,可以基于实际需求重新设计

过度耦合

特性嫉妒(Feature Envy)

某个方法访问另一个对象的数据比访问自身数据还多。

不恰当的亲密关系(Inappropriate Intimacy)

一个类使用了另一个类的内部字段和方法。

消息链(Message Chains)

代码中出现一系列调用,如 $a->b()->c()->d()

中间人(Middle Man)

如果一个类只执行一个操作,并且只是将工作委托给另一个类,那么这个类的存在是否必要?