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」关系。如果只是为了复用代码而创建子类,那还不如用组合。记住:继承是强耦合,用之前想清楚。

实战决策流程

到底该用提炼类还是提炼子类?我一般按这个流程判断:

发现过大的类 类中是否有两组相对独立的 数据和方法? 使用提炼类(Extract Class) 将独立职责拆分为新类 类中是否有条件分支 (if-else/switch)决定行为? 使用提炼子类(Extract Subclass) 用多态替代条件分支 考虑其他重构手法 (如:提炼接口、拆分模块)

实战中的注意事项

嗯,这里要注意几个坑:

  1. 不要一次性拆太细。我建议先拆出最明显的那一组,运行测试,确认没问题再继续。一次拆太多容易引入bug。
  2. 注意循环依赖。提炼类时,如果两个类互相引用,说明拆得不对。应该再想想怎么划清边界。
  3. 子类不要太多。如果子类数量超过5个,可以考虑用策略模式替代继承。

我的习惯:每次重构前,先写一个「职责清单」。把当前类的所有public方法列出来,然后分组。每组就是一个潜在的类或子类。这个清单能帮你理清思路。

最后说一句,重构过大的类,本质上是在做一件事:让每个类只做一件事,并且做好。这听起来简单,但真正做到需要持续练习。别指望一次重构就完美,代码是慢慢养出来的。


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