搬移特性(二):以函数调用取代内联代码、移动语句、拆分阶段、以工厂函数取代构造函数
好,咱们接着聊搬移特性。上一章讲了几个基础手法,这一章要上点硬菜了。这四个重构手法——以函数调用取代内联代码、移动语句、拆分阶段、以工厂函数取代构造函数——说白了,都是在帮你把代码里的“块”搬得更合理。
我个人习惯把这类重构叫做“拆积木”。你想想看,代码写久了,难免会堆出一坨一坨的东西。有些是逻辑上该独立的,你硬塞在一起;有些是顺序不对的,你写反了;有些是创建对象的方式太死板。嗯,这一章就是专门治这些毛病的。
以函数调用取代内联代码
这个手法名字有点绕,其实意思很简单:把一段内联的代码,替换成一个函数调用。
我在项目中遇到过这样的场景:一个方法里写了十几行逻辑,仔细一看,这逻辑跟另一个类里的某个方法几乎一模一样。当时我就想,这不明摆着该复用吗?
为什么要做这个重构?
- 消除重复:同样的逻辑散落各处,改一处漏三处,这是噩梦。
- 提升可读性:函数名本身就是注释。看到
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") 放在 userName 和 user 之间,其实挺别扭。更好的做法是把日志挪到方法开头:
public void process() {
log.info("Start processing");
String userName = fetchUserName();
User user = findUser(userName);
log.info("User found: " + user.getId());
// ... 后续逻辑
}
你看,这样读起来就顺了:先记录开始,然后一步步往下走。逻辑流更自然。
拆分阶段
这个手法稍微有点抽象。它的核心思想是:把一段代码中不同阶段做的事情,拆分成独立的函数或类。
我举个例子你就明白了。假设有一段代码,先解析输入数据,然后做业务计算,最后格式化输出。这三个阶段混在一个函数里,每个阶段之间通过临时变量传递数据。这种代码,改起来特别容易出问题。
拆分阶段的做法:
- 识别出代码中的不同阶段。
- 为每个阶段创建一个独立的函数。
- 让前一个阶段的输出,成为后一个阶段的输入。
关键点:每个阶段只做一件事。阶段之间通过明确的数据结构传递信息,而不是共享一堆临时变量。
来看个实际例子。重构前:
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);
你看,create 和 fromRecord 清楚地表达了创建方式。而且,如果将来要加缓存逻辑,直接在工厂函数里加就行,调用方完全不用改。
注意:不是所有构造函数都要替换。如果类很简单,只有一个构造函数,而且没有特殊需求,直接用 new 也没问题。别为了用工厂函数而用,那叫过度设计。
知识体系总览
这四个手法,其实都在做同一件事:让代码的职责更清晰,依赖更合理。我画了张图,帮你理清它们的关系:
这张图把四个手法的关系梳理清楚了。你看,它们都指向同一个核心目标:让代码的职责更清晰,依赖更合理。只不过每个手法解决的具体问题不同。
我的建议:别试图一次性把所有手法都用上。先挑一个你觉得最痛的地方下手。比如,如果你发现一个函数里混了多个阶段,那就先做拆分阶段。做完之后,你可能会发现有些内联代码可以抽成函数,那就接着做。一步一步来,代码会慢慢变好。
好了,这一章的内容就到这里。这四个手法,你可以在日常开发中随时用起来。记住,重构不是一次性的活动,而是持续的习惯。每次看到代码不顺眼的地方,就想想能不能用这些手法把它理顺。