56、代码坏味道:异曲同工的类 - 实战合并类与提炼超类

说实话,我见过太多这样的代码了——两个类长得像双胞胎,但就是没人敢动它们。你问为什么?怕改出bug呗。但你想过没有,这种「异曲同工」的类,其实是代码里最隐蔽的坏味道之一。

什么叫异曲同工的类?说白了,就是两个类做着差不多的事,但各自为政。它们可能有相似的字段、相似的方法,甚至逻辑都差不多,但就是没有共享任何代码。我当年接手过一个老项目,里面有三个类都在做「数据校验」,但每个类都自己写了一套校验逻辑。嗯,那感觉就像看到三胞胎穿着不同的衣服,但骨子里一模一样。

坏味道的典型症状

怎么判断你的代码里有没有这种坏味道?我个人习惯看这几个信号:

  • 字段重复:两个类里有相同的字段,比如都有 nameageemail
  • 方法重复:方法名不同,但做的事情几乎一样
  • 逻辑重复:if-else 的判断条件、循环逻辑、计算方式高度相似
  • 调用方式相似:客户端代码对这两个类的使用模式几乎一致

举个例子,假设你有两个类:UserValidatorAdminValidator。它们都做校验,但一个校验普通用户,一个校验管理员。你仔细看看,其实校验逻辑有80%是重复的。

核心问题:重复代码意味着维护成本翻倍。改一个地方,另一个地方很容易漏掉。我曾经因为这种问题,线上出过一个严重的bug——改了用户校验规则,但忘了改管理员校验,结果管理员账号全被锁了。

两种重构手法

遇到这种坏味道,我一般有两种选择:合并类,或者提炼超类。具体选哪个,得看情况。

手法一:合并类

如果两个类做的事情几乎一模一样,只是参数或配置不同,那直接合并成一个类就行了。比如:

// 重构前
public class UserValidator {
    public boolean validate(String name, String email) {
        if (name == null || name.isEmpty()) return false;
        if (email == null || !email.contains("@")) return false;
        return true;
    }
}

public class AdminValidator {
    public boolean validate(String name, String email, String role) {
        if (name == null || name.isEmpty()) return false;
        if (email == null || !email.contains("@")) return false;
        if (role == null || role.isEmpty()) return false;
        return true;
    }
}

// 重构后:合并成一个类
public class Validator {
    public boolean validate(String name, String email) {
        if (name == null || name.isEmpty()) return false;
        if (email == null || !email.contains("@")) return false;
        return true;
    }

    public boolean validateAdmin(String name, String email, String role) {
        if (!validate(name, email)) return false;
        if (role == null || role.isEmpty()) return false;
        return true;
    }
}

你看,合并之后,公共的校验逻辑只写一次。管理员校验直接复用普通用户的校验,再加一个角色校验就行了。这样改起来也方便——改一次,两边都生效。

我的经验:合并类适合那些「本质相同、只是细节略有差异」的场景。如果两个类的职责边界已经模糊了,合并是最直接的办法。

手法二:提炼超类

如果两个类有公共部分,但各自又有独特的职责,那合并就不太合适了。这时候提炼超类更优雅。说白了,就是把公共的字段和方法抽到父类里,子类各自保留自己的特色。

// 重构前
public class User {
    private String name;
    private String email;
    private String phone;

    public boolean isValid() {
        return name != null && email != null && email.contains("@");
    }

    public String getDisplayName() {
        return name + " (" + email + ")";
    }
}

public class Admin {
    private String name;
    private String email;
    private String role;

    public boolean isValid() {
        return name != null && email != null && email.contains("@") && role != null;
    }

    public String getDisplayName() {
        return name + " (" + email + ") - " + role;
    }
}

// 重构后:提炼超类
public abstract class Person {
    protected String name;
    protected String email;

    public boolean isValid() {
        return name != null && email != null && email.contains("@");
    }

    public abstract String getDisplayName();
}

public class User extends Person {
    private String phone;

    @Override
    public String getDisplayName() {
        return name + " (" + email + ")";
    }
}

public class Admin extends Person {
    private String role;

    @Override
    public boolean isValid() {
        return super.isValid() && role != null;
    }

    @Override
    public String getDisplayName() {
        return name + " (" + email + ") - " + role;
    }
}

提炼超类的好处是:公共逻辑统一管理,子类只关注自己的差异点。我记得有一次重构一个支付系统,里面有三个支付渠道类,每个类都有签名、验签、发送请求的逻辑。我把公共部分提炼成 BasePaymentChannel,子类只实现各自的签名算法。那之后,加一个新渠道只需要写几十行代码,而不是几百行。

避坑指南:我曾经犯过一个错误——把超类提炼得太「胖」了。什么方法都往父类塞,结果子类被迫继承了一堆用不上的东西。记住,超类只放「真正公共」的部分,不要贪多。

如何选择?一张图说清楚

我画了一张流程图,帮你快速判断该用哪种手法:

发现异曲同工的类 职责是否相同? (只是参数/配置不同) 合并类 统一成一个类 提炼超类 公共部分抽到父类 代码量减少 维护成本降低 职责清晰 扩展性增强 代码更干净,团队更省心

实战中的注意事项

光知道理论还不够,我踩过的坑得跟你说说:

  1. 不要过度抽象:如果两个类只有10%的相似度,别硬抽超类。强行抽象只会让代码更难懂。
  2. 注意继承深度:超类下面再抽超类,层级太深会让人崩溃。我见过一个项目,继承链有6层,改个方法要翻半天。
  3. 测试要跟上:重构完一定要跑测试。合并类或提炼超类后,原来的行为不能变。我习惯先写测试用例,再动手重构。
  4. 小步提交:别一次性改太多。先抽一个方法,提交;再抽一个字段,提交。这样出问题了容易回滚。

我的习惯:每次重构前,我会先问自己三个问题:这个类真的需要存在吗?它和其他类的边界在哪里?改完之后,新同学能一眼看懂吗?如果答案都是肯定的,那就动手。

总结一下

异曲同工的类,说白了就是代码里的「重复劳动」。合并类适合「本质相同」的场景,提炼超类适合「有公共部分但各有特色」的场景。选对手法,代码就能瘦身不少。

嗯,最后说一句:重构不是炫技,是为了让代码更好维护。你想想看,如果每个新需求来的时候,你都能快速定位、快速修改,那才是真本事。


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