搬移特性(二):以函数调用取代内联代码、移动语句、拆分阶段、以工厂函数取代构造函数

好,咱们接着聊搬移特性。上一章讲了几个基础手法,这一章要上点硬菜了。这四个重构手法——以函数调用取代内联代码、移动语句、拆分阶段、以工厂函数取代构造函数——说白了,都是在帮你把代码里的“块”搬得更合理。

我个人习惯把这类重构叫做“拆积木”。你想想看,代码写久了,难免会堆出一坨一坨的东西。有些是逻辑上该独立的,你硬塞在一起;有些是顺序不对的,你写反了;有些是创建对象的方式太死板。嗯,这一章就是专门治这些毛病的。

以函数调用取代内联代码

这个手法名字有点绕,其实意思很简单:把一段内联的代码,替换成一个函数调用

我在项目中遇到过这样的场景:一个方法里写了十几行逻辑,仔细一看,这逻辑跟另一个类里的某个方法几乎一模一样。当时我就想,这不明摆着该复用吗?

为什么要做这个重构?

  • 消除重复:同样的逻辑散落各处,改一处漏三处,这是噩梦。
  • 提升可读性:函数名本身就是注释。看到 calculateDiscount() 比看一堆 if-else 舒服多了。
  • 降低耦合:内联代码往往依赖局部变量,抽成函数后,依赖关系更清晰。

核心判断标准:如果一段内联代码做的事情,已经有一个现成的函数能完成,那就别犹豫,直接替换掉。

来看个例子。重构前:

public double getPrice(Order order) {
    double basePrice = order.getQuantity() * order.getItemPrice();
    double discount = 0;
    if (basePrice > 1000) {
        discount = basePrice * 0.05;
    }
    return basePrice - discount;
}

重构后:

public double getPrice(Order order) {
    double basePrice = order.getQuantity() * order.getItemPrice();
    double discount = calculateDiscount(basePrice);
    return basePrice - discount;
}

private double calculateDiscount(double basePrice) {
    if (basePrice > 1000) {
        return basePrice * 0.05;
    }
    return 0;
}

你可能会说,这不就是把几行代码挪了个地方吗?对,但意义不一样。现在 calculateDiscount 可以被其他地方复用了。而且,如果折扣规则变了,你只需要改这一个地方。

小技巧:如果内联代码里用到了局部变量,抽成函数时记得把变量作为参数传进去。别偷懒用全局变量,那会引入隐式依赖。

移动语句

这个手法更简单,但容易被忽视。把一条语句从当前位置,移动到另一个位置。听起来像废话?其实不然。

我曾经接手过一个项目,一个方法里两百多行,变量声明、业务逻辑、日志打印、异常处理全混在一起。我花了整整一天,就干了一件事:把相关的语句挪到一起。效果立竿见影,代码可读性直接翻倍。

什么时候该用移动语句?

  • 变量声明远离使用处:声明在文件头,用到的地方在两百行之后。这种代码读起来得来回翻,累死人。
  • 相关逻辑被无关代码隔开:比如先初始化 A,然后打印日志,再初始化 B。日志跟 A、B 都没关系,为什么不把两个初始化放一起?
  • 条件分支里的公共代码:if 和 else 里都有同一行代码,挪到外面去。

注意:移动语句不是随便挪。你得确保移动后语义不变。尤其是那些有副作用的语句,比如修改了全局状态、写了文件、发了网络请求。挪之前,想清楚。

看个例子:

public void process() {
    String userName = fetchUserName();
    log.info("Start processing");
    User user = findUser(userName);
    log.info("User found: " + user.getId());
    // ... 后续逻辑
}

这里 log.info("Start processing") 放在 userNameuser 之间,其实挺别扭。更好的做法是把日志挪到方法开头:

public void process() {
    log.info("Start processing");
    String userName = fetchUserName();
    User user = findUser(userName);
    log.info("User found: " + user.getId());
    // ... 后续逻辑
}

你看,这样读起来就顺了:先记录开始,然后一步步往下走。逻辑流更自然。

拆分阶段

这个手法稍微有点抽象。它的核心思想是:把一段代码中不同阶段做的事情,拆分成独立的函数或类

我举个例子你就明白了。假设有一段代码,先解析输入数据,然后做业务计算,最后格式化输出。这三个阶段混在一个函数里,每个阶段之间通过临时变量传递数据。这种代码,改起来特别容易出问题。

拆分阶段的做法:

  1. 识别出代码中的不同阶段。
  2. 为每个阶段创建一个独立的函数。
  3. 让前一个阶段的输出,成为后一个阶段的输入。

关键点:每个阶段只做一件事。阶段之间通过明确的数据结构传递信息,而不是共享一堆临时变量。

来看个实际例子。重构前:

public String processOrder(String rawInput) {
    String[] parts = rawInput.split(",");
    String productId = parts[0];
    int quantity = Integer.parseInt(parts[1]);
    double price = Double.parseDouble(parts[2]);
    
    double total = quantity * price;
    if (total > 500) {
        total = total * 0.9;
    }
    
    String result = "Order: " + productId + ", Total: " + total;
    return result;
}

这里明显有三个阶段:解析输入、计算总价、格式化输出。混在一起,测试起来也麻烦。重构后:

public String processOrder(String rawInput) {
    OrderData data = parseInput(rawInput);
    double total = calculateTotal(data);
    return formatOutput(data.productId, total);
}

private OrderData parseInput(String rawInput) {
    String[] parts = rawInput.split(",");
    return new OrderData(parts[0], 
                         Integer.parseInt(parts[1]), 
                         Double.parseDouble(parts[2]));
}

private double calculateTotal(OrderData data) {
    double total = data.quantity * data.price;
    if (total > 500) {
        total = total * 0.9;
    }
    return total;
}

private String formatOutput(String productId, double total) {
    return "Order: " + productId + ", Total: " + total;
}

// 中间数据结构
private static class OrderData {
    String productId;
    int quantity;
    double price;
    
    OrderData(String productId, int quantity, double price) {
        this.productId = productId;
        this.quantity = quantity;
        this.price = price;
    }
}

现在每个阶段都可以独立测试。而且,如果将来要改解析逻辑,或者改输出格式,你只需要动对应的函数,不会影响到其他部分。

我的经验:拆分阶段时,中间数据结构很关键。别偷懒用 Map 或 Object 数组,定义一个清晰的类,哪怕只是内部类。这样代码的意图一目了然。

以工厂函数取代构造函数

这个手法,说白了就是用静态工厂方法代替 new 关键字

为什么这么做?构造函数有几个天生的缺陷:

  • 名字固定:必须跟类名一样,没法表达创建意图。
  • 每次调用都创建新对象:没法复用已有实例。
  • 不能返回子类型:构造函数只能返回当前类的实例。

工厂函数可以解决这些问题。你可以给它起个有意义的名字,比如 of()create()from()。你可以在里面做缓存、返回子类、甚至返回 null。

我曾经在一个项目中,看到一个类有七个重载的构造函数。调用的时候,根本分不清该用哪个。后来我全部替换成工厂函数,每个函数名都说明了用途,代码瞬间清晰了。

看个例子。重构前:

public class User {
    private String name;
    private int age;
    
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

// 调用
User user = new User("张三", 25);

重构后:

public class User {
    private String name;
    private int age;
    
    // 构造函数设为私有
    private User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 工厂函数
    public static User create(String name, int age) {
        return new User(name, age);
    }
    
    // 另一个工厂函数,从数据库记录创建
    public static User fromRecord(Map<String, Object> record) {
        return new User((String) record.get("name"), 
                        (Integer) record.get("age"));
    }
}

// 调用
User user = User.create("张三", 25);
User userFromDb = User.fromRecord(record);

你看,createfromRecord 清楚地表达了创建方式。而且,如果将来要加缓存逻辑,直接在工厂函数里加就行,调用方完全不用改。

注意:不是所有构造函数都要替换。如果类很简单,只有一个构造函数,而且没有特殊需求,直接用 new 也没问题。别为了用工厂函数而用,那叫过度设计。

知识体系总览

这四个手法,其实都在做同一件事:让代码的职责更清晰,依赖更合理。我画了张图,帮你理清它们的关系:

搬移特性(二):四个手法的关系 核心目标:职责清晰,依赖合理 以函数调用取代 内联代码 移动语句 调整代码顺序 拆分阶段 分离不同职责 以工厂函数取代 构造函数 适用场景 有现成函数可用 语句顺序不合理 代码含多个阶段 构造函数不够用 共同目标:让代码更容易理解、测试和维护

这张图把四个手法的关系梳理清楚了。你看,它们都指向同一个核心目标:让代码的职责更清晰,依赖更合理。只不过每个手法解决的具体问题不同。

我的建议:别试图一次性把所有手法都用上。先挑一个你觉得最痛的地方下手。比如,如果你发现一个函数里混了多个阶段,那就先做拆分阶段。做完之后,你可能会发现有些内联代码可以抽成函数,那就接着做。一步一步来,代码会慢慢变好。

好了,这一章的内容就到这里。这四个手法,你可以在日常开发中随时用起来。记住,重构不是一次性的活动,而是持续的习惯。每次看到代码不顺眼的地方,就想想能不能用这些手法把它理顺。


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