55、代码坏味道:过大的类 - 实战提炼类与提炼子类
说实话,过大的类是我在代码评审里最常见的坏味道之一。你想想看,一个类动辄上千行,里面塞了十几个字段、几十个方法,这玩意儿谁维护谁头疼。我见过最夸张的一个类,足足有四千多行,里面既有业务逻辑、又有数据访问、还混着UI渲染——简直就是个垃圾堆。
为什么会这样?说白了,很多同学写代码时图省事。新功能来了,找个现成的类往里塞,塞着塞着就膨胀了。今天我们就来聊聊怎么收拾这种烂摊子。
识别过大的类
先说说怎么判断一个类是不是太大了。我个人习惯看三个指标:
- 行数超过300行——这已经是个危险信号了
- 字段超过10个——说明这个类承担了太多职责
- 方法超过20个——尤其是那些public方法,每个都代表一种能力
核心原则:一个类应该只有一个改变的理由。如果你发现修改一个功能需要改动这个类的多个方法,那它很可能违反了单一职责原则。
我在项目中遇到过这样一个案例:一个OrderService类,里面既有订单计算、又有库存检查、还有物流配送、甚至包含了支付处理。每次需求变更,这个类都要改,测试用例也越来越难写。这就是典型的「上帝类」。
提炼类(Extract Class)
提炼类是最直接的解法。当一个类里有两组相对独立的数据和方法时,就应该把它们拆开。
来看个例子。假设我们有这样一个类:
public class Employee {
private String name;
private String email;
private String phone;
// 个人信息相关
public String getName() { return name; }
public String getEmail() { return email; }
public String getPhone() { return phone; }
// 工作信息相关
private String department;
private double salary;
private LocalDate hireDate;
public String getDepartment() { return department; }
public double getSalary() { return salary; }
public LocalDate getHireDate() { return hireDate; }
// 工资计算
public double calculateBonus() {
// 复杂的奖金计算逻辑
return salary * 0.1;
}
// 部门变更
public void changeDepartment(String newDept) {
this.department = newDept;
// 发送通知等
}
}
你看,这个类里明显有两组概念:个人信息和工作信息。我们可以把工作信息提炼成一个独立的类:
public class Employee {
private String name;
private String email;
private String phone;
private EmploymentInfo employmentInfo;
// 个人信息相关方法...
}
public class EmploymentInfo {
private String department;
private double salary;
private LocalDate hireDate;
public double calculateBonus() {
return salary * 0.1;
}
public void changeDepartment(String newDept) {
this.department = newDept;
// 发送通知等
}
}
小技巧:提炼类时,先看看哪些方法只访问同一组字段。如果某个方法只用了3个字段,而另外5个方法用了另外5个字段,这就是拆分的信号。
提炼子类(Extract Subclass)
提炼子类适用于另一种场景:一个类里有些行为只在特定条件下才使用。与其用if-else判断,不如用继承来区分。
我记得有个电商项目,订单类里塞满了各种判断:
public class Order {
private String type; // "normal" 或 "premium"
public double calculateShipping() {
if ("premium".equals(type)) {
// 高级会员免运费
return 0;
} else {
// 普通会员按重量计算
return weight * 0.5;
}
}
public double calculateDiscount() {
if ("premium".equals(type)) {
return total * 0.2; // 8折
} else {
return total * 0.05; // 95折
}
}
// 还有十几个类似的方法...
}
这种代码,每次加新会员类型都要改这个类,而且很容易漏掉某个if分支。提炼子类后:
public abstract class Order {
public abstract double calculateShipping();
public abstract double calculateDiscount();
}
public class NormalOrder extends Order {
public double calculateShipping() {
return weight * 0.5;
}
public double calculateDiscount() {
return total * 0.05;
}
}
public class PremiumOrder extends Order {
public double calculateShipping() {
return 0; // 免运费
}
public double calculateDiscount() {
return total * 0.2;
}
}
避坑指南:我曾经犯过一个错误——过度使用子类。提炼子类的前提是子类之间有明确的「is-a」关系。如果只是为了复用代码而创建子类,那还不如用组合。记住:继承是强耦合,用之前想清楚。
实战决策流程
到底该用提炼类还是提炼子类?我一般按这个流程判断:
实战中的注意事项
嗯,这里要注意几个坑:
- 不要一次性拆太细。我建议先拆出最明显的那一组,运行测试,确认没问题再继续。一次拆太多容易引入bug。
- 注意循环依赖。提炼类时,如果两个类互相引用,说明拆得不对。应该再想想怎么划清边界。
- 子类不要太多。如果子类数量超过5个,可以考虑用策略模式替代继承。
我的习惯:每次重构前,先写一个「职责清单」。把当前类的所有public方法列出来,然后分组。每组就是一个潜在的类或子类。这个清单能帮你理清思路。
最后说一句,重构过大的类,本质上是在做一件事:让每个类只做一件事,并且做好。这听起来简单,但真正做到需要持续练习。别指望一次重构就完美,代码是慢慢养出来的。
公众号:蓝海资料掘金营,微信deep3321