9、封装(下):以查询取代临时变量、提炼类、将类内联化、隐藏委托关系
好,咱们接着聊封装的下半部分。上一章我们讲了封装字段和方法,今天要聊的这几个手法,说白了就是「让代码的归属感更强」。我经常在重构老系统时遇到这样的场景:一个类里塞了太多不该它管的事,或者一个临时变量在方法里被传来传去,看着就累。
嗯,咱们一个一个来。
以查询取代临时变量
先说说临时变量。你想想看,一个方法里如果出现了三四个临时变量,每个变量都只为了计算某个中间结果,然后后面又用这个变量去干别的事。这种代码读起来就像在解一道数学题,每一步都得记住上一步的结果。
我个人习惯是:如果一个临时变量的值可以通过一个函数计算出来,那就把它替换成函数调用。这样做的最大好处是——方法变短了,逻辑变清晰了,而且这个计算逻辑可以被复用。
核心原则:临时变量只应该存在于它真正「临时」的场景,比如循环计数器、累加器。如果它承载了业务含义,就应该提炼成查询方法。
举个例子,假设我们有这样一段代码:
public double getPrice() {
int basePrice = quantity * itemPrice;
double discount = Math.max(0, basePrice - 100) * 0.05;
double shippingCost = Math.min(basePrice * 0.1, 5.0);
return basePrice - discount + shippingCost;
}
这里 basePrice 被用了三次,而且每次都是读取它的值。我们可以把它替换成一个查询方法:
public double getPrice() {
return basePrice() - discount() + shippingCost();
}
private int basePrice() {
return quantity * itemPrice;
}
private double discount() {
return Math.max(0, basePrice() - 100) * 0.05;
}
private double shippingCost() {
return Math.min(basePrice() * 0.1, 5.0);
}
你看,现在每个方法只做一件事,而且方法名就是注释。我在项目中遇到过很多次这种情况——原来一个 30 行的方法,拆成五六个小方法后,读起来就像在读一篇短文,而不是在解谜。
小提示:如果临时变量只被赋值一次,而且后续只读不写,那基本都可以安全替换。如果变量被多次赋值,那就得先考虑拆分方法了。
提炼类
接下来是提炼类。这个手法我用的频率非常高。为什么?因为一个类一旦承担了太多职责,它就会变得臃肿、难以测试、难以理解。
我记得有一次接手一个订单系统,一个 Order 类里居然包含了支付逻辑、物流逻辑、优惠券逻辑、用户通知逻辑……我当时就一个感觉:这哪是订单类,这分明是个「万能类」。
提炼类的思路很简单:把一组紧密相关的字段和方法,从原来的类中搬到一个新类里。判断标准就是——这些字段和方法是不是经常一起变化?是不是属于同一个业务概念?
比如,一个 Person 类里如果既有姓名、年龄,又有电话号码、区号、国家代码,那电话号码这部分就应该提炼成 TelephoneNumber 类。
// 重构前
public class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String getTelephoneNumber() {
return "(" + officeAreaCode + ") " + officeNumber;
}
}
// 重构后
public class Person {
private String name;
private TelephoneNumber telephoneNumber;
public String getTelephoneNumber() {
return telephoneNumber.getTelephoneNumber();
}
}
public class TelephoneNumber {
private String areaCode;
private String number;
public String getTelephoneNumber() {
return "(" + areaCode + ") " + number;
}
}
这样做的好处是什么?电话号码的逻辑被隔离了。以后如果要加国际区号、分机号,只需要改 TelephoneNumber 类,不会影响到 Person 类。这就是单一职责原则的落地。
注意:提炼类不是越细越好。如果一个类里只有一个字段和一个 getter,那可能就过度设计了。我一般会看这个「小概念」是否会在多个地方被使用,或者是否有独立的业务规则。
将类内联化
有提炼就有内联。将类内联化是提炼类的反向操作。什么时候需要内联?当一个类已经不再承担足够的职责,或者它的存在反而增加了理解成本。
我曾经在一个项目中看到这样的代码:一个 ShippingCost 类,里面只有一个字段 amount 和一个 getter。这个类只在 Order 类中被使用。你说它有意义吗?从设计角度看,它确实封装了「运费」这个概念,但从实际使用看,它只是增加了一层间接调用。
内联化的做法就是:把这个类的字段和方法全部搬回使用它的类中,然后删除这个类。
// 重构前
public class Order {
private ShippingCost shippingCost;
public double getTotalCost() {
return basePrice() + shippingCost.getAmount();
}
}
public class ShippingCost {
private double amount;
public double getAmount() {
return amount;
}
}
// 重构后
public class Order {
private double shippingCost;
public double getTotalCost() {
return basePrice() + shippingCost;
}
}
你可能会问:那什么时候该提炼,什么时候该内联?我的经验是:看这个类是否「独立存在」。如果它只被一个类使用,而且没有自己的行为逻辑(只有 getter/setter),那就内联掉。如果它有独立的业务规则,或者被多个类共享,那就保留。
一句话总结:提炼类是为了让职责更清晰,内联类是为了让结构更简单。两者并不矛盾,而是根据实际情况灵活选择。
隐藏委托关系
最后一个手法是隐藏委托关系。这个我特别想多说两句,因为很多人在写代码时容易忽略「调用链」的问题。
什么叫委托关系?就是 A 调用 B,B 调用 C,C 调用 D。如果 A 直接知道 B、C、D 的存在,那 A 就和 B、C、D 都耦合了。一旦 C 的接口变了,A 也得改。
隐藏委托关系的做法是:在 B 上提供一个方法,让 A 只和 B 打交道,B 内部去调用 C。这样 A 就不需要知道 C 的存在了。
// 重构前:A 直接调用 B.getC().doSomething()
public class A {
public void action() {
B b = new B();
b.getC().doSomething();
}
}
public class B {
private C c;
public C getC() {
return c;
}
}
// 重构后:B 隐藏了 C 的存在
public class A {
public void action() {
B b = new B();
b.doSomething();
}
}
public class B {
private C c;
public void doSomething() {
c.doSomething();
}
}
你看,现在 A 只依赖 B,B 只依赖 C。调用链变短了,耦合度降低了。我在项目中遇到过最夸张的情况是:一个方法里出现了 order.getCustomer().getAddress().getCity() 这样的链式调用。这种代码一旦中间某个对象返回 null,整个系统就崩了。
避坑指南:我曾经在一个遗留系统中看到过五层以上的委托链。当时我的做法是:先在中间层加委托方法,然后逐步把调用方改成只依赖最近的一层。不要一次性改完,容易出问题。
知识体系总览
好了,这四个手法讲完了。我画了一张图,帮你理清它们之间的关系:
这四个手法,说白了就是一件事:让代码的归属感更强。临时变量该归到方法里,字段该归到类里,调用关系该归到最近的委托者那里。你想想看,如果每个类、每个方法都只管自己该管的事,那代码读起来得多舒服。
好,今天就聊到这里。下次咱们继续聊封装的其他手法。
公众号:蓝海资料掘金营,微信 deep3321