代码膨胀
指的是代码、方法或类变得过于庞大,难以维护。这些问题通常不会立即出现,而是随着项目的演进逐渐积累,特别是在没有人主动优化它们的情况下。
过长的方法
方法的代码行数过多,通常来说,如果一个方法超过 10 行,就应该开始考虑是否需要拆分或优化。
过大的类
一个类包含了太多的字段、方法或者代码行数,导致类的职责不清晰,难以理解和维护。
原始类型偏执
过度使用基本数据类型,而不是封装成更具体的小对象。例如:
- 直接使用整数、字符串来表示复杂概念(如货币、范围、电话号码等),而不是定义专门的类。
- 使用常量来表示特定的信息,比如
const int USER_ADMIN_ROLE = 1
代表管理员权限,而不是使用枚举或封装类。 - 在数据结构(如数组或哈希表)中使用字符串常量作为字段名称,而不是使用对象封装这些数据。
过长的参数列表
方法的参数超过 3~4 个,可能导致调用复杂、可读性下降。通常可以使用对象封装参数,或者利用构造函数和 setter 方法逐步构建参数。
数据堆积
代码的不同部分包含了相同的一组变量(例如数据库连接参数经常一起出现)。这种情况通常意味着这些数据应该被封装到一个独立的类中,以减少重复代码并提高可维护性。
面向对象滥用
这些代码问题通常是由于不完整或不正确地应用面向对象编程(OOP)原则导致的。
switch
语句(Switch Statements)代码中存在复杂的
switch
语句或大量的if-else
语句。这通常表明代码没有充分利用多态,而是基于条件判断执行不同逻辑,而这些逻辑本可以通过继承和重写方法来实现。
解决方案通常是使用策略模式、状态模式或工厂模式来代替
switch
语句。临时字段(Temporary Field)
类的某些字段只在特定情况下才被赋值,在其他情况下这些字段是空的或无意义的。
这可能意味着:
- 这些字段应该被移入一个单独的类(如一个封装上下文的类)。
- 这些字段应该在需要的时候才创建,避免无意义的实例变量。
- 可能需要重构方法,减少不必要的字段依赖。
拒绝继承(Refused Bequest)
子类只使用了父类的部分方法或属性,导致继承层次结构不合理。
可能的后果:
- 子类要么不使用父类的方法,造成代码浪费。
- 子类要么重写父类方法并抛出异常,违背了里氏替换原则(Liskov Substitution Principle, LSP)。
解决方案:
- 重新评估继承关系,看看是否可以改用组合而非继承。
- 拆分父类,只让子类继承它真正需要的部分。
不同接口但功能相似的类(Alternative Classes with Different Interfaces)
两个类的功能基本相同,但它们的方法名称不同,导致使用它们的代码缺乏统一性。
这通常是因为不同团队或不同时间开发时没有统一约定,导致多个类做着类似的事情但接口不同。
解决方案:
- 重构这两个类,合并成一个类,或者让它们实现同一个接口。
- 提供适配器模式(Adapter Pattern),让它们可以通过相同的方式被调用。
变更阻碍者
这些代码问题意味着,如果你需要修改某个代码的某一部分,就不得不在许多地方做出相应的修改,导致开发变得更加复杂和昂贵。
多向变更(Divergent Change)
修改一个类时,需要修改许多不相关的方法。
例如:如果你添加一个新产品类型,不仅要修改查询方法,还要修改显示方法、下单方法等,说明这个类承担了过多的职责。
解决方案:
- 拆分类,将不同的功能放入各自独立的类中。
- 使用策略模式(Strategy Pattern),将变化的逻辑封装到独立的类中,减少对核心类的修改。
散弹式修改(Shotgun Surgery)
修改某个功能时,需要在多个类中进行多处小修改,例如:你需要修改数据库字段名称,但不仅要改数据库代码,还得改多个业务逻辑类、UI 代码等。
这种情况通常表明某个功能被过度分散,导致修改变得复杂。
解决方案:
- 将相关逻辑集中到单个类或模块中,减少对多个类的依赖。
- 使用封装(Encapsulation),例如提供公共 API,避免直接访问多个类的内部数据。
平行继承体系(Parallel Inheritance Hierarchies)
每次为某个类创建一个子类时,都必须为另一个类创建一个对应的子类。
例如:你有一个
Product
类,每次你创建一个新的DigitalProduct
子类,你也不得不为ProductManager
创建DigitalProductManager
,导致继承结构成对增长。问题:这种做法会导致继承体系过于复杂,修改某个类时,另一个类也必须跟着修改,降低了灵活性。
解决方案:
- 使用组合代替继承(优先考虑“组合优于继承”原则)。
- 使用抽象工厂(Abstract Factory)来管理对象的创建,避免手动扩展多个平行的类层次。
冗余代码
冗余代码指的是不必要的、不起作用的代码,如果删除它们,代码会更加清晰、简洁且易于维护。
过多注释(Comments)
方法里充满了解释性的注释,可能是因为代码难以理解,或者过于冗长。
问题:好的代码应该自解释,如果代码需要大量注释才能理解,说明可能存在命名不清晰或逻辑过于复杂的问题。
解决方案:
- 使用清晰的变量名、方法名,让代码本身表达意图,而不是依赖注释。
- 重构代码,拆分复杂的方法,减少对注释的依赖。
重复代码(Duplicate Code)
两个代码片段几乎相同,可能是:
- 两个方法内部逻辑类似。
- 多个类有类似的方法或代码块。
问题:代码冗余,修改时需要同步修改多个地方,容易出错。
解决方案:
- 提取公共方法,减少重复代码。
- 使用继承或组合,将相同逻辑集中到基类或独立模块中。
懒惰类(Lazy Class)
一个类存在但几乎没有作用,比如:
- 这个类原本是为了扩展而创建的,但后来并没有增加新的功能。
- 代码结构调整后,这个类变得不再有实际用途。
解决方案:
- 合并类,把它的功能整合到相关的类中。
- 删除类,如果它不再有实际作用。
数据类(Data Class)
这个类只有字段和对应的 getter/setter,但不包含任何逻辑,仅作为数据存储。
问题:这种类缺乏封装,所有逻辑都在其他类中,导致代码分散。
解决方案:
- 将数据操作方法放入数据类,让它能独立处理自己的数据(遵循面向对象的封装原则)。
- 使用领域对象(Domain Objects),让数据类不仅仅存储数据,还能执行相关的业务逻辑。
死代码(Dead Code)
变量、参数、字段、方法或类已经不再被使用,通常是由于:
- 代码重构后忘记删除旧代码。
- 曾经用于某个功能,但功能被废弃了。
问题:
- 代码越来越庞大,增加维护难度。
- 影响阅读,让开发者误以为某些代码仍然重要。
解决方案:
- 定期清理代码,删除不再使用的变量、方法或类。
- 使用静态代码分析工具(如 Clang-Tidy、SonarQube)来检测死代码。
过度设计(Speculative Generality)
代码包含未被使用的类、方法、字段或参数,通常是因为开发者为了“未来可能用到”而提前设计。
问题:
- 过度设计增加了代码复杂度,但这些功能实际上从未被使用。
- 维护这样的代码浪费时间和精力。
解决方案:
- 遵循 YAGNI 原则(You Ain't Gonna Need It),不要提前实现不确定的功能。
- 删除未使用的代码,如果未来确实需要,可以基于实际需求重新设计。
过度耦合
特性嫉妒(Feature Envy)
某个方法访问另一个对象的数据比访问自身数据还多。
不恰当的亲密关系(Inappropriate Intimacy)
一个类使用了另一个类的内部字段和方法。
消息链(Message Chains)
代码中出现一系列调用,如
$a->b()->c()->d()
。
中间人(Middle Man)
如果一个类只执行一个操作,并且只是将工作委托给另一个类,那么这个类的存在是否必要?