搬移特性(一):搬移函数、搬移字段、搬移语句到函数内、搬移语句到调用者

重构这件事,说白了就是「把代码放到它该待的地方」。我做了十几年架构,见过太多代码长成「一锅粥」——函数不该管的事管了,字段不该待的类待了,语句不该出现的位置出现了。今天咱们就聊聊搬移特性,这是重构里最基础也最实用的一组手法。

你想想看,代码的坏味道往往就藏在「位置不对」上。一个函数明明只跟订单相关,却写在用户类里;一个字段只在某个流程里用到,却被塞进了全局配置。这些问题的解法其实很简单——搬过去就行了。

搬移函数

搬移函数,是我个人用得最频繁的重构手法之一。它的核心判断标准就一条:这个函数跟哪个类/模块的关系更紧密?

我在项目中遇到过这样一个场景:一个订单处理系统里,有个 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 字段,但整个系统里只有登录日志模块用到它。每次加载用户对象,都得带上这个无关字段,浪费内存不说,还容易引发并发问题。

搬移字段的决策树是这样的:

  1. 这个字段被哪些类使用?
  2. 如果只有一个类频繁使用它,搬过去。
  3. 如果多个类都用,但某个类「拥有」它的语义更合理,也搬过去。
  4. 如果实在分不清,考虑提取一个新类。
// 重构前: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 函数里居然包含了发送短信通知的逻辑。每次扣款都发短信,测试环境里一天能收到几百条。这就是典型的「搬移语句到调用者」——短信通知应该是调用方按需决定的事,不该硬塞在扣款函数里。

注意:搬移语句到调用者时,要小心「重复代码」的风险。如果多个调用方都需要执行同样的语句,那就不该搬,而是考虑提取一个新函数。

知识体系总览

下面这张图,是我梳理的搬移特性核心逻辑。你可以把它当作一个决策参考:

搬移特性决策逻辑 代码位置不对? 搬移函数 函数跟谁关系更紧密? 搬移字段 数据该属于哪个类? 搬移到函数内 准备语句属于函数内部 搬移到调用者 副作用交给调用方决定

这四种搬移手法,本质上都是在回答同一个问题:这段代码,放在这里合适吗?不合适就搬,别将就。代码的「位置感」是需要刻意培养的,做得多了,你一眼就能看出哪里不对劲。

我的经验:每次搬移之前,先问自己三个问题——搬过去之后,调用方会更简单吗?被搬的代码会更内聚吗?修改需求会更集中吗?如果三个答案都是「是」,那就放心搬。

好了,这一章的内容就到这里。搬移特性是重构的「基本功」,但也是最容易被忽视的。很多人一上来就想着设计模式、架构分层,却连函数该放哪都搞不清楚。先把这些基础手法练熟,后面的重构之路才会走得稳。

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