6、代码坏味道详解(下):平行继承体系、冗余类、过大的类、拒绝接受赠品、注释滥用、循环语句
各位好,咱们接着聊代码坏味道。上一章讲了几个常见的“臭味”,今天继续把剩下的六个讲透。说实话,这些坏味道在真实项目里出现的频率非常高,我几乎在每个遗留系统里都能抓到几个。咱们一个一个来看。
6.1 平行继承体系
什么叫平行继承体系?说白了,就是每当你新增一个类,就得同时新增另一个类。比如你有一个 Animal 基类,下面有 Dog、Cat;然后你还有一个 AnimalCare 基类,下面有 DogCare、CatCare。这两个继承树是平行的,每次加一种动物,就得同时加两个类。
我在项目中遇到过这样一个案例:一个电商系统,有 Order 和 OrderProcessor 两个继承体系。每新增一种订单类型(比如团购订单、预售订单),就得同时新增两个类。后来我建议用策略模式把处理逻辑注入到订单类中,两个继承树合并成一个,代码量直接砍半。
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。
重构过大的类,核心思路是“按职责拆分”。比如上面的订单服务类,可以拆成:
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 注释滥用
注释本身不是坏味道,但滥用注释绝对是。我见过三种典型的注释滥用:
- 解释性注释:代码本身已经能表达意图,却还要加注释。比如
i++; // 增加 i 的值,这种注释纯属多余。 - 过期注释:代码改了,注释没改。这种注释比没有注释更可怕,因为它会误导人。
- 大段注释掉的代码:有些人习惯把旧代码注释掉,留着“以防万一”。结果一留就是几年,没人敢删。
好的注释,应该解释“为什么这么做”,而不是“做了什么”。比如:
// 使用双重检查锁,因为单例的创建需要线程安全,但又不想每次获取都加锁
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());
当然,不是所有循环都要替换。但如果你发现一个循环里做了三件以上不同的事,那就该考虑拆分了。
下面我用一张图来总结今天讲的这六个坏味道,以及它们之间的关系:
好了,这六个坏味道就讲完了。说实话,每个坏味道背后都对应着一种设计上的“偷懒”。但代码这东西,你偷的懒,迟早要还的。与其以后花更多时间重构,不如一开始就写得干净点。
嗯,今天就到这里。希望这些内容对你有帮助。