51、代码坏味道:临时字段 — 实战提炼类与引入Null对象

说实话,临时字段这个坏味道,我见过太多次了。

它不像长方法或者过大的类那么显眼,但破坏力一点都不小。你想想看,一个类里有些字段只在特定场景下才有值,其他时候都是 null 或者某个默认值。这种字段就像「幽灵」,平时不出现,一到某个分支逻辑里就冒出来。

我接手过一个老项目,里面有个 OrderProcessor 类,十几个字段里有一半都是这种临时字段。每次改代码都得小心翼翼,生怕哪个 null 指针炸了。后来我花了两个迭代彻底重构了它——核心手法就是今天要讲的:提炼类引入 Null 对象

什么是临时字段?

说白了,就是一个字段只在某个方法的某个分支里被赋值,其他时候都是 null 或者无意义的值。

举个例子:

public class Invoice {
    private String customerName;
    private double amount;
    private String discountCode;      // 只在有折扣时使用
    private double discountPercent;   // 只在有折扣时使用
    private String specialNote;       // 只在特殊备注时使用

    public double calculateTotal() {
        double total = amount;
        if (discountCode != null) {
            total = total * (1 - discountPercent / 100);
        }
        return total;
    }

    public String generateSummary() {
        StringBuilder sb = new StringBuilder();
        sb.append("客户: ").append(customerName);
        if (specialNote != null) {
            sb.append(" [备注: ").append(specialNote).append("]");
        }
        return sb.toString();
    }
}

你看,discountCodediscountPercentspecialNote 这三个字段,大部分时候是 null。它们的存在让类的职责变得模糊——这个 Invoice 到底是在做账单计算,还是在处理折扣和备注?

核心判断标准: 如果一个字段在对象的生命周期中,大部分时间都是 null 或无意义值,那它就是临时字段。

为什么临时字段是坏味道?

  • 可读性差:新接手的人得猜「这个字段什么时候有值?什么时候是 null?」
  • 容易出 bug:忘记判空就 NPE,判空太多又让代码臃肿。
  • 违反单一职责:一个类承担了多个不相关的职责。
  • 测试困难:为了测试某个分支,你得构造一堆 null 字段。

我记得有一次线上事故,就是因为一个临时字段在某个罕见路径下没被初始化,结果生产环境炸了。从那以后,我对临时字段就特别敏感。

重构手法一:提炼类(Extract Class)

把那些临时字段和它们相关的方法,一起提炼成一个独立的类。

还是上面那个 Invoice 的例子。我们可以把折扣相关的字段和方法提炼出来:

public class Discount {
    private String code;
    private double percent;

    public Discount(String code, double percent) {
        this.code = code;
        this.percent = percent;
    }

    public double applyTo(double amount) {
        return amount * (1 - percent / 100);
    }

    public boolean isActive() {
        return code != null;
    }
}

然后 Invoice 里就清爽多了:

public class Invoice {
    private String customerName;
    private double amount;
    private Discount discount;   // 可能为 null

    public double calculateTotal() {
        double total = amount;
        if (discount != null && discount.isActive()) {
            total = discount.applyTo(total);
        }
        return total;
    }
}

嗯,这里要注意:提炼之后,Invoice 里仍然有一个可能为 null 的 discount 字段。这就是我们下一步要解决的问题。

重构手法二:引入 Null 对象(Introduce Null Object)

与其到处判空,不如创建一个「什么都不做」的 Null 对象。这样调用方就不需要关心是不是 null 了。

我们给 Discount 创建一个 Null 对象:

public class NullDiscount extends Discount {
    public NullDiscount() {
        super(null, 0.0);
    }

    @Override
    public double applyTo(double amount) {
        return amount;  // 不打折
    }

    @Override
    public boolean isActive() {
        return false;
    }
}

然后在 Invoice 中,默认使用 Null 对象:

public class Invoice {
    private String customerName;
    private double amount;
    private Discount discount = new NullDiscount();  // 默认就是 Null 对象

    public void setDiscount(Discount discount) {
        this.discount = discount != null ? discount : new NullDiscount();
    }

    public double calculateTotal() {
        return discount.applyTo(amount);  // 不再需要判空
    }
}
我的习惯: 在构造函数或 setter 中就把 Null 对象赋值好,这样整个类的其他方法都不用再担心 null 的问题。代码瞬间干净很多。

完整重构流程

我一般按这个步骤来:

  1. 识别临时字段:找出那些大部分时间为 null 的字段。
  2. 分组:把逻辑上相关的临时字段分成一组。
  3. 提炼类:为每组创建一个新类,把字段和方法搬过去。
  4. 引入 Null 对象:为每个新类创建一个 Null 对象子类。
  5. 替换判空逻辑:把原来的 if (xxx != null) 替换为多态调用。
  6. 测试:确保行为不变。

知识体系图

下面这张图展示了临时字段重构的核心逻辑:

临时字段重构知识体系 代码坏味道:临时字段 字段大部分时间为 null?逻辑上可分组? 手法一:提炼类 将临时字段+方法独立成类 手法二:引入 Null 对象 创建“什么都不做”的子类 结果:消除判空,职责清晰 多态替代条件判断 常组合使用

实战中的避坑指南

我曾经踩过的坑:
  • 不要过度提炼:如果临时字段只有一两个,且逻辑简单,直接内联处理可能更好。我见过有人把两个字段就提炼成一个类,反而增加了复杂度。
  • Null 对象要完整:Null 对象必须实现所有抽象方法,并且返回「无操作」的结果。如果漏掉一个方法,运行时还是会 NPE。
  • 注意性能:如果 Null 对象被频繁创建,可以考虑单例模式。不过大多数场景下,GC 开销可以忽略。

什么时候该用,什么时候不该用?

场景 推荐做法 原因
临时字段有 3 个以上,且逻辑相关 提炼类 + Null 对象 职责清晰,消除判空
临时字段只有 1 个,且逻辑简单 直接内联,或简单判空 过度设计反而不好
临时字段出现在多个类中 考虑提取公共基类或策略模式 消除重复
临时字段是性能热点 谨慎使用 Null 对象,考虑缓存 避免频繁分配对象
一个小技巧: 如果你不确定该不该提炼,先试试「内联临时变量」——把临时字段改成局部变量。如果改完后方法变得很长,那就说明该提炼了。

好了,关于临时字段的重构就聊到这里。核心就两招:提炼类 把散落的字段收拢,引入 Null 对象 把判空逻辑消灭掉。这两招组合起来,代码质量能上一个台阶。

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