8. 封装(二):以查询取代临时变量、提炼类、将类内联化、隐藏委托关系

这一章我们继续聊封装。上一讲我们处理了数据和行为之间的边界,这一讲我们深入到更细的粒度——临时变量、类的边界、以及对象之间的委托关系。

说实话,封装这件事,做得好了,代码像搭积木一样清晰;做得不好,就像一团乱麻,改一个地方牵出一堆问题。我见过太多项目,就是因为封装不到位,最后重构成本高得吓人。

好,我们一个一个来看。

8.1 以查询取代临时变量

临时变量是代码里最常见的“坏味道”之一。你想想看,一个方法里写了七八个临时变量,每个变量只为了计算下一步的输入,读起来就像在解一道数学题。

核心思想:把临时变量的计算逻辑提炼成一个独立的查询方法。

举个例子:

// 重构前
public double getPrice() {
    double basePrice = quantity * itemPrice;
    double discount = Math.max(0, basePrice - 100) * 0.05;
    double shippingCost = Math.min(basePrice * 0.1, 5);
    return basePrice - discount + shippingCost;
}

这段代码里,basePrice 被用了三次。每次读这段代码,你都得在心里重新算一遍 basePrice 是什么。更麻烦的是,如果以后价格计算规则变了,你得在这个方法里找来找去。

重构后:

// 重构后
public double getPrice() {
    return getBasePrice() - getDiscount() + getShippingCost();
}

private double getBasePrice() {
    return quantity * itemPrice;
}

private double getDiscount() {
    return Math.max(0, getBasePrice() - 100) * 0.05;
}

private double getShippingCost() {
    return Math.min(getBasePrice() * 0.1, 5);
}

关键点:每个查询方法只做一件事,并且方法名就是它的文档。你不需要看实现,就知道它返回什么。

我在项目中遇到过一种情况:一个方法里有十几个临时变量,层层依赖。我当时花了整整一个下午,才把它们全部拆成查询方法。拆完之后,那个类从 300 行变成了 500 行,但每个方法都短小精悍,后来维护的人都说好。

什么时候该做?

  • 临时变量被多次引用
  • 临时变量的计算逻辑可能变化
  • 你想让方法更短、更聚焦

8.2 提炼类

一个类承担了太多职责,就会变得臃肿。我见过一个类,里面既有订单逻辑,又有支付逻辑,还有物流逻辑——说白了,它什么都管,但什么都管不好。

核心思想:把一组紧密相关的字段和方法,从原类中抽出来,形成一个新的类。

举个实际例子:

// 重构前:Person 类太臃肿
public class Person {
    private String name;
    private String officeAreaCode;
    private String officeNumber;
    
    public String getTelephoneNumber() {
        return "(" + officeAreaCode + ") " + officeNumber;
    }
    // 还有一堆其他方法...
}

这里,电话号码相关的字段和方法其实可以独立出来:

// 重构后
public class TelephoneNumber {
    private String areaCode;
    private String number;
    
    public String getTelephoneNumber() {
        return "(" + areaCode + ") " + number;
    }
}

public class Person {
    private String name;
    private TelephoneNumber telephoneNumber;
    
    public String getTelephoneNumber() {
        return telephoneNumber.getTelephoneNumber();
    }
}

你看,提炼之后,TelephoneNumber 可以独立测试、独立复用。如果以后电话号码格式变了,你只需要改一个地方。

注意:提炼类不是越多越好。我见过有人把两个字段就抽成一个类,结果类爆炸了。提炼的边界应该是“一组逻辑上紧密相关的数据和行为”。

我曾经在一个电商项目中,把订单类里的地址信息提炼成 Address 类。一开始觉得多此一举,后来发现地址校验、格式化、打印标签这些功能都可以复用,省了不少事。

8.3 将类内联化

有提炼就有内联。有时候,一个类太“瘦”了,反而增加了复杂度。

核心思想:如果一个类做的事情太少,不值得独立存在,就把它的字段和方法合并到使用它的类中。

举个例子:

// 重构前:ShippingRate 类太单薄
public class ShippingRate {
    private double rate;
    public double getRate() { return rate; }
}

public class Order {
    private ShippingRate shippingRate;
    public double getShippingCost() {
        return shippingRate.getRate() * weight;
    }
}

如果 ShippingRate 只有这一个字段,而且只在 Order 中使用,那不如直接内联:

// 重构后
public class Order {
    private double shippingRate;
    public double getShippingCost() {
        return shippingRate * weight;
    }
}

判断标准:如果一个类没有自己的生命周期,没有独立的行为,只是作为数据的“搬运工”,那就该内联掉。

嗯,这里要注意:内联和提炼是相反的操作。你不需要纠结哪个更好,关键是看当前类的职责是否清晰。我个人的习惯是:先提炼,等代码稳定了,再回头看哪些类可以内联。

8.4 隐藏委托关系

这是封装中最容易被忽视的一点。你想想看,客户端代码要调用一个功能,结果要经过好几层对象才能拿到数据——这就是“委托链”。

核心思想:在服务对象上直接提供客户端需要的方法,隐藏内部的委托关系。

看代码:

// 重构前:客户端需要知道委托关系
public class Person {
    private Department department;
    public Department getDepartment() { return department; }
}

public class Department {
    private Person manager;
    public Person getManager() { return manager; }
}

// 客户端调用
Person person = ...;
Person manager = person.getDepartment().getManager();

客户端代码必须知道 PersonDepartmentDepartmentManager。如果以后部门结构变了,所有客户端都得改。

重构后:

// 重构后:隐藏委托关系
public class Person {
    private Department department;
    
    public Person getManager() {
        return department.getManager();
    }
}

// 客户端调用
Person person = ...;
Person manager = person.getManager();

客户端不再需要知道 Department 的存在。这就是“最少知识原则”的体现。

避坑指南:我曾经在一个项目中,把所有的委托关系都隐藏了,结果服务类变得很“胖”,每个方法都是转发。后来我意识到,隐藏委托关系不是无脑转发,而是只隐藏那些“客户端不需要知道”的中间层。

知识体系总览

下面这张图总结了本章四个重构手法的关系:

封装(二)知识体系 以查询取代临时变量 临时变量 → 查询方法 提炼类 大类的职责拆分 将类内联化 瘦类合并 隐藏委托关系 减少客户端依赖 互为逆操作 方法提炼 封装边界 核心目标:降低耦合,提高内聚,让代码更容易理解和修改

总结

这四个手法,说白了都是在回答一个问题:代码的边界在哪里?

  • 以查询取代临时变量:把计算逻辑封装成方法,让方法名代替注释。
  • 提炼类:把一组紧密相关的数据和行为,从大类中分离出来。
  • 将类内联化:如果一个类太“瘦”,就把它合并回去。
  • 隐藏委托关系:让客户端只依赖直接的对象,不关心内部结构。

我个人觉得,封装这件事,没有绝对的对错。关键是看你的代码在哪个阶段。项目初期,可以稍微松散一点;到了维护期,封装得越干净,改起来越轻松。

好,这一讲就到这里。记住一句话:好的封装,让调用者不需要知道实现细节


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