6、代码坏味道详解(下):平行继承体系、冗余类、过大的类、拒绝接受赠品、注释滥用、循环语句

各位好,咱们接着聊代码坏味道。上一章讲了几个常见的“臭味”,今天继续把剩下的六个讲透。说实话,这些坏味道在真实项目里出现的频率非常高,我几乎在每个遗留系统里都能抓到几个。咱们一个一个来看。

6.1 平行继承体系

什么叫平行继承体系?说白了,就是每当你新增一个类,就得同时新增另一个类。比如你有一个 Animal 基类,下面有 DogCat;然后你还有一个 AnimalCare 基类,下面有 DogCareCatCare。这两个继承树是平行的,每次加一种动物,就得同时加两个类。

核心问题: 这种设计本质上是在用继承来模拟“类型分组”,但往往是因为没有把变化点抽象出来。

我在项目中遇到过这样一个案例:一个电商系统,有 OrderOrderProcessor 两个继承体系。每新增一种订单类型(比如团购订单、预售订单),就得同时新增两个类。后来我建议用策略模式把处理逻辑注入到订单类中,两个继承树合并成一个,代码量直接砍半。

重构建议: 尝试用组合或策略模式替代平行继承。如果两个体系确实需要独立变化,考虑引入桥接模式。

6.2 冗余类

冗余类,就是那些“食之无味,弃之可惜”的类。它们可能只有一两个方法,或者完全就是另一个类的“影子”。

举个例子:

// 冗余类示例
public class StringUtils {
    public static boolean isEmpty(String str) {
        return str == null || str.trim().isEmpty();
    }
}

// 实际上项目中已经有 Apache Commons Lang 的 StringUtils
// 这个类完全多余

嗯,这里要注意:冗余类不仅仅是代码重复的问题,它还会增加认知负担。新同事看到这个类,会想“为什么不用已有的工具类?” 然后花时间去确认。

避坑指南: 我曾经在一个项目里发现了一个“只被一个方法使用”的类,那个方法只有三行代码。我直接把它内联到调用方,删掉了整个类。重构之后,代码反而更清晰了。

判断一个类是否冗余,有个简单标准:如果这个类可以被删除,而系统功能不受影响,那它就是冗余的。别犹豫,删掉它。

6.3 过大的类

过大的类,是很多老项目的通病。一个类几百行甚至上千行,什么功能都往里塞。你想想看,这样的类,改一个地方可能影响十个功能,谁敢动?

我见过最夸张的一个类,是一个“订单服务类”,里面包含了订单创建、支付、退款、物流、发票、优惠券……总共 3000 多行。每次上线,这个类都是修改的热点,而且经常出 bug。

判断标准: 如果一个类有超过 10 个方法,或者超过 200 行代码,你就应该警惕了。当然,这不是硬性规定,但可以作为参考。

重构过大的类,核心思路是“按职责拆分”。比如上面的订单服务类,可以拆成:

  • OrderCreator:负责订单创建
  • PaymentHandler:负责支付逻辑
  • RefundService:负责退款
  • LogisticsManager:负责物流

每个类只做一件事,这样改起来就安全多了。

6.4 拒绝接受赠品

这个名字有点意思。它指的是:子类继承了父类的方法,但子类并不需要这些方法。说白了,就是“你给了我,但我不用”。

举个例子:

public class Bird {
    public void fly() {
        System.out.println("Flying...");
    }
}

public class Penguin extends Bird {
    // 企鹅不会飞,但继承了 fly 方法
    // 要么空实现,要么抛出异常
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly");
    }
}

这种设计,说白了就是违反了 Liskov 替换原则。子类不能完全替代父类,那继承的意义就大打折扣了。

重构建议: 把不需要的方法从父类中移除,或者把继承关系改为组合关系。比如上面的例子,可以把 fly 行为提取成一个接口 Flyable,只有会飞的鸟才实现它。

我个人习惯,在设计继承体系时,会先问自己:“子类真的需要父类的所有行为吗?” 如果答案是否定的,那就要重新考虑设计了。

6.5 注释滥用

注释本身不是坏味道,但滥用注释绝对是。我见过三种典型的注释滥用:

  1. 解释性注释:代码本身已经能表达意图,却还要加注释。比如 i++; // 增加 i 的值,这种注释纯属多余。
  2. 过期注释:代码改了,注释没改。这种注释比没有注释更可怕,因为它会误导人。
  3. 大段注释掉的代码:有些人习惯把旧代码注释掉,留着“以防万一”。结果一留就是几年,没人敢删。
避坑指南: 我曾经接手过一个项目,里面有一大段被注释掉的代码,注释写着“这段代码暂时不用,但可能以后会用到”。结果三年过去了,那段代码从来没被启用过。我直接删掉了,系统运行正常。

好的注释,应该解释“为什么这么做”,而不是“做了什么”。比如:

// 使用双重检查锁,因为单例的创建需要线程安全,但又不想每次获取都加锁
if (instance == null) {
    synchronized (Singleton.class) {
        if (instance == null) {
            instance = new Singleton();
        }
    }
}

这种注释就有价值,因为它解释了设计决策背后的原因。

6.6 循环语句

循环语句本身不是坏味道,但过度使用循环或者循环嵌套过深,就是坏味道了。你想想看,一个三层嵌套的 for 循环,里面还有 if-else 分支,读起来得多费劲?

我建议,能用函数式编程替代的循环,尽量用函数式。比如:

// 坏味道:手写循环
List<String> names = new ArrayList<>();
for (User user : users) {
    if (user.isActive()) {
        names.add(user.getName());
    }
}

// 好味道:用 Stream
List<String> names = users.stream()
    .filter(User::isActive)
    .map(User::getName)
    .collect(Collectors.toList());

当然,不是所有循环都要替换。但如果你发现一个循环里做了三件以上不同的事,那就该考虑拆分了。

核心原则: 循环应该只做一件事。如果循环体超过 5 行,或者有嵌套,就把它提取成一个独立的方法。

下面我用一张图来总结今天讲的这六个坏味道,以及它们之间的关系:

代码坏味道(下)知识体系 6种坏味道 平行继承体系 新增一个类,必须新增另一个 冗余类 可有可无,增加认知负担 过大的类 超过200行,职责过多 拒绝接受赠品 子类不需要父类方法 注释滥用 解释性、过期、注释代码 循环语句 嵌套过深、职责不单一 重构核心:单一职责、组合优于继承、代码即文档

好了,这六个坏味道就讲完了。说实话,每个坏味道背后都对应着一种设计上的“偷懒”。但代码这东西,你偷的懒,迟早要还的。与其以后花更多时间重构,不如一开始就写得干净点。

嗯,今天就到这里。希望这些内容对你有帮助。

公众号:蓝海资料掘金营,微信deep3321