基本重构手法(二):改变函数声明、封装变量、变量改名、引入参数对象
这一讲我们继续聊重构的基本手法。说实话,这些手法看起来简单,但恰恰是日常开发中用得最多的。我见过太多代码,就是因为这些基础手法没用到位,慢慢变得难以维护。
今天要讲的四个手法,说白了就是帮我们把代码的「命名」和「结构」理清楚。你想想看,代码里最常打交道的是什么?无非就是函数、变量、参数这三样东西。把它们管好了,代码质量就上去了一大半。
改变函数声明
函数声明,就是函数的「门面」。名字叫什么?参数有哪些?返回值是什么?这些决定了别人怎么用你的函数。
我在项目中遇到过一种情况:一个函数叫 processData,传进去三个参数,但实际只用了两个。第三个参数是早期版本遗留的,后来业务变了,这个参数就没人用了。但大家都不敢删,怕影响调用方。结果呢?每个调用方都得传一个 null 进去。
这就是典型的「函数声明需要改变」的信号。
具体怎么做?我总结了几种常见场景:
1. 改名
函数名不好,直接改。别犹豫。
// 改名前
function calc(a, b) {
return a * b + a;
}
// 改名后
function calculateTotalPrice(unitPrice, quantity) {
return unitPrice * quantity + unitPrice;
}
嗯,这里要注意:改名后要全局搜索所有调用处,全部替换。IDE 的重构功能可以帮你做这件事。
2. 增减参数
参数多了少了,都要调整。但别一次加太多参数,超过三个就要考虑用对象了。
// 参数太多,调用方痛苦
function createUser(name, age, email, phone, address, city, zipCode) {
// ...
}
// 改成参数对象
function createUser(userInfo) {
// userInfo.name, userInfo.age ...
}
3. 改变返回值
有时候函数返回了太多东西,调用方只用了其中一部分。这时候可以考虑拆分函数,或者返回一个对象。
封装变量
变量,尤其是全局变量或模块级变量,是代码里的「定时炸弹」。你永远不知道谁在什么时候改了它。
我曾经在一个遗留系统里看到这样的代码:一个全局的 config 对象,被二十多个文件直接读写。有一天某个同事不小心把 config.timeout 写成了 config.timeOut,结果系统间歇性超时,查了两天才找到原因。
封装变量,就是把直接暴露的变量藏起来,通过函数来访问和修改。
// 封装前
let defaultTimeout = 3000;
// 封装后
let _defaultTimeout = 3000;
export function getDefaultTimeout() {
return _defaultTimeout;
}
export function setDefaultTimeout(value) {
if (value < 0) {
throw new Error('超时时间不能为负数');
}
_defaultTimeout = value;
}
这样做的好处很明显:
- 可以加校验逻辑(比如上面的负数检查)
- 可以加日志,知道谁在什么时候改了值
- 未来可以改变内部实现,不影响外部调用
变量改名
变量改名,听起来最简单,但做起来最考验功力。为什么?因为一个好名字,要准确表达变量的「含义」和「用途」。
我见过这样的代码:
let d = new Date();
let t = d.getTime();
let r = t - 86400000;
你能一眼看出 r 是什么吗?86400000 又是什么?
改成这样就好多了:
let now = new Date();
let currentTimestamp = now.getTime();
let yesterdayTimestamp = currentTimestamp - ONE_DAY_IN_MILLISECONDS;
变量改名的几个原则:
- 长度不是问题,清晰才是。 别怕名字长,怕的是看不懂。
- 用名词表示数据,用动词表示函数。
userList是数据,getUserList是操作。 - 避免歧义。
data、info、temp这类名字,能不用就不用。
引入参数对象
这个手法,说白了就是把一组经常一起出现的参数,封装成一个对象。
你想想看,如果一个函数有五个参数,调用的时候你得记住每个参数的顺序和类型。这多痛苦?
// 重构前
function bookFlight(departure, destination, date, passengers, classType) {
// ...
}
// 调用
bookFlight('北京', '上海', '2024-03-15', 2, '经济舱');
// 重构后
function bookFlight(bookingInfo) {
// bookingInfo.departure, bookingInfo.destination ...
}
// 调用
bookFlight({
departure: '北京',
destination: '上海',
date: '2024-03-15',
passengers: 2,
classType: '经济舱'
});
引入参数对象的好处:
- 调用方不用记参数顺序。 按名字传参,不容易出错。
- 新增参数不影响现有调用。 在对象里加个字段就行,不用改所有调用处。
- 可以复用这个对象。 如果多个函数都用同一组参数,直接复用这个对象类型。
知识体系总览
下面这张图,把这四个手法的关系梳理了一下。你可以看到,它们都围绕着「代码的可读性和可维护性」这个核心目标。
综合案例:一个订单处理函数
最后,我们看一个综合案例。假设你接手了这样一段代码:
function process(o, u, p) {
let t = o.total;
let d = o.discount;
let f = t * d;
let r = u.balance - f;
if (r >= 0) {
u.balance = r;
p.push(o);
return true;
}
return false;
}
这段代码的问题太多了:变量名看不懂、参数含义不明、逻辑混在一起。我们用今天学的手法来重构:
// 第一步:变量改名
function processOrder(order, user, processedOrders) {
let total = order.total;
let discount = order.discount;
let finalPrice = total * discount;
let remainingBalance = user.balance - finalPrice;
if (remainingBalance >= 0) {
user.balance = remainingBalance;
processedOrders.push(order);
return true;
}
return false;
}
// 第二步:引入参数对象(如果参数继续增多)
function processOrder(orderInfo) {
const { order, user, processedOrders } = orderInfo;
// ... 逻辑不变
}
// 第三步:封装变量(如果 user.balance 是全局可变的)
function deductBalance(user, amount) {
if (user.balance < amount) {
return false;
}
user.balance -= amount;
return true;
}
你看,经过这几个手法的组合使用,代码变得清晰多了。每个函数只做一件事,每个变量名都表达了它的含义。
好了,这一讲就到这里。这四个手法虽然基础,但用好了,能让你的代码质量上一个台阶。下次写代码时,不妨多想想:这个函数名够不够好?这个变量能不能封装?参数是不是太多了?