搬移特性(一):搬移函数、搬移字段、搬移语句到函数内、搬移语句到调用者
重构这件事,说白了就是「把代码放到它该待的地方」。我做了十几年架构,见过太多代码长成「一锅粥」——函数不该管的事管了,字段不该待的类待了,语句不该出现的位置出现了。今天咱们就聊聊搬移特性,这是重构里最基础也最实用的一组手法。
你想想看,代码的坏味道往往就藏在「位置不对」上。一个函数明明只跟订单相关,却写在用户类里;一个字段只在某个流程里用到,却被塞进了全局配置。这些问题的解法其实很简单——搬过去就行了。
搬移函数
搬移函数,是我个人用得最频繁的重构手法之一。它的核心判断标准就一条:这个函数跟哪个类/模块的关系更紧密?
我在项目中遇到过这样一个场景:一个订单处理系统里,有个 calculateDiscount 函数,它读取订单金额、用户等级、促销活动三个数据源。但问题是,这个函数被放在了用户服务里。每次改促销规则,都得跑到用户服务里去翻代码。这合理吗?不合理。
判断依据其实很简单:
- 函数使用了哪个类的数据最多?
- 函数被哪个类的其他方法调用得最频繁?
- 修改需求通常来自哪个领域?
如果答案指向另一个类,那就搬过去。别犹豫。
搬移函数的关键信号:当你发现一个函数「引用」了另一个类的数据,比引用自己类的数据还多时,就该搬了。
// 重构前:折扣计算函数放在用户服务里
public class UserService {
public double calculateDiscount(Order order, User user) {
double baseDiscount = user.getLevel().getDiscountRate();
double promoDiscount = order.getPromotion().getRate();
return Math.min(baseDiscount, promoDiscount);
}
}
// 重构后:搬移到订单服务里,因为折扣逻辑跟订单更紧密
public class OrderService {
public double calculateDiscount(Order order, User user) {
double baseDiscount = user.getLevel().getDiscountRate();
double promoDiscount = order.getPromotion().getRate();
return Math.min(baseDiscount, promoDiscount);
}
}
嗯,这里要注意:搬移函数不是简单的「剪切-粘贴」。你得检查调用方,更新所有引用点。我习惯用 IDE 的「移动」功能,它能自动处理引用关系。手动搬的话,很容易漏掉某个调用方。
搬移字段
搬移字段,说白了就是「这个数据不该放在这里」。我见过最典型的例子:一个 User 类里有个 lastLoginTime 字段,但整个系统里只有登录日志模块用到它。每次加载用户对象,都得带上这个无关字段,浪费内存不说,还容易引发并发问题。
搬移字段的决策树是这样的:
- 这个字段被哪些类使用?
- 如果只有一个类频繁使用它,搬过去。
- 如果多个类都用,但某个类「拥有」它的语义更合理,也搬过去。
- 如果实在分不清,考虑提取一个新类。
// 重构前:lastLoginTime 放在 User 类里
public class User {
private String name;
private String email;
private LocalDateTime lastLoginTime; // 只有登录日志用
}
// 重构后:搬移到 LoginRecord 类里
public class LoginRecord {
private Long userId;
private LocalDateTime loginTime;
}
我曾经接手过一个老项目,一个订单类里有 30 多个字段,但仔细一查,其中 12 个字段只在「退款流程」里用到。这就是典型的搬移字段机会。我们把那 12 个字段提取到 RefundInfo 类里,订单类瞬间清爽了,退款逻辑也独立了。
小技巧:搬移字段时,记得检查是否有「隐式依赖」。比如某个字段的值会影响另一个字段的计算逻辑,这种耦合关系要一并处理。
搬移语句到函数内
这个手法,我把它叫做「把散落的拼图收进盒子里」。你经常能看到这样的代码:调用一个函数之前,先在外面写两三行准备语句,然后才调用函数。这几行准备语句,其实应该属于函数内部。
为什么要搬?因为调用方不该知道函数内部的「准备工作」。这是封装性的体现。你想想看,如果每次调用 sendEmail 之前都得先拼邮件模板、检查收件人格式,那这个函数的封装就是失败的。
// 重构前:准备语句散落在调用方
public void processOrder(Order order) {
String emailContent = buildEmailContent(order);
String recipient = order.getUser().getEmail();
if (recipient != null && !recipient.isEmpty()) {
emailService.send(recipient, emailContent);
}
}
// 重构后:把准备语句搬进函数内
public void processOrder(Order order) {
emailService.sendOrderConfirmation(order);
}
// emailService 内部
public void sendOrderConfirmation(Order order) {
String emailContent = buildEmailContent(order);
String recipient = order.getUser().getEmail();
if (recipient != null && !recipient.isEmpty()) {
send(recipient, emailContent);
}
}
我个人习惯是:当我在调用方看到「先做 A,再做 B,然后调用 C」这种模式时,就会问自己——A 和 B 是不是 C 的「前置条件」?如果是,就搬进去。
搬移语句到调用者
这个手法跟上面那个正好相反。有时候,函数内部包含了「不该它管」的语句。比如一个函数既做核心计算,又做日志记录。日志记录这种「副作用」,应该交给调用方去处理。
判断标准很简单:这条语句是不是每次调用都必须执行?如果不是,就搬出去。
// 重构前:函数内部做了日志记录
public void updateInventory(Product product, int quantity) {
product.setStock(product.getStock() - quantity);
logger.info("库存更新: " + product.getId() + ", 变更: " + quantity);
}
// 重构后:日志交给调用方
public void updateInventory(Product product, int quantity) {
product.setStock(product.getStock() - quantity);
}
// 调用方按需记录日志
public void processSale(Sale sale) {
updateInventory(sale.getProduct(), sale.getQuantity());
if (sale.getQuantity() > 100) {
logger.info("大额销售: " + sale.getId());
}
}
我曾经在一个支付系统里看到,一个 deductBalance 函数里居然包含了发送短信通知的逻辑。每次扣款都发短信,测试环境里一天能收到几百条。这就是典型的「搬移语句到调用者」——短信通知应该是调用方按需决定的事,不该硬塞在扣款函数里。
注意:搬移语句到调用者时,要小心「重复代码」的风险。如果多个调用方都需要执行同样的语句,那就不该搬,而是考虑提取一个新函数。
知识体系总览
下面这张图,是我梳理的搬移特性核心逻辑。你可以把它当作一个决策参考:
这四种搬移手法,本质上都是在回答同一个问题:这段代码,放在这里合适吗?不合适就搬,别将就。代码的「位置感」是需要刻意培养的,做得多了,你一眼就能看出哪里不对劲。
我的经验:每次搬移之前,先问自己三个问题——搬过去之后,调用方会更简单吗?被搬的代码会更内聚吗?修改需求会更集中吗?如果三个答案都是「是」,那就放心搬。
好了,这一章的内容就到这里。搬移特性是重构的「基本功」,但也是最容易被忽视的。很多人一上来就想着设计模式、架构分层,却连函数该放哪都搞不清楚。先把这些基础手法练熟,后面的重构之路才会走得稳。