6. 基本重构手法(三):组合函数、拆分循环、以管道取代循环、移除标记参数
好,咱们继续聊重构手法。今天这四种手法,我个人觉得是日常开发中最常用的“组合拳”。你想想看,我们每天写的代码里,有多少是又长又臭的函数?有多少循环里塞满了不相干的逻辑?又有多少参数传进来只是为了控制一个 if-else?
嗯,今天我们就来逐个击破。
6.1 组合函数(Combine Functions into Class / Transform)
先说说组合函数。这个手法说白了就是:把散落在各处的、操作同一份数据的函数,收拢到一起。
我在项目中遇到过这样一个场景:一个订单系统里,计算折扣、计算税费、计算运费这三个函数,各自接收同样的订单对象,各自返回计算结果。调用方每次都要调三次,还得自己拼结果。这其实就是一个典型的“数据与行为分离”的坏味道。
核心思路: 如果多个函数都操作同一份数据,就把它们组合成一个类(Class)或者一个转换函数(Transform)。
举个例子,重构前是这样的:
function discount(order) { /* 计算折扣 */ }
function tax(order) { /* 计算税费 */ }
function shipping(order) { /* 计算运费 */ }
// 调用方
const d = discount(order);
const t = tax(order);
const s = shipping(order);
const total = d + t + s;
重构后,我们可以组合成一个类:
class OrderCalculator {
constructor(order) {
this.order = order;
}
discount() { /* 计算折扣 */ }
tax() { /* 计算税费 */ }
shipping() { /* 计算运费 */ }
total() {
return this.discount() + this.tax() + this.shipping();
}
}
// 调用方
const calc = new OrderCalculator(order);
const total = calc.total();
你看,调用方不再需要关心内部细节了。这就是组合函数的价值——封装变化,简化调用。
我的习惯: 当发现三个以上函数都接收同一个参数时,我就会考虑是否该组合了。别等到五个、六个才动手。
6.2 拆分循环(Split Loop)
这个手法,我估计很多人会忽略。你想想看,一个循环里做了两件不同的事,是不是很常见?
let totalSalary = 0;
let youngestAge = Infinity;
for (const p of people) {
totalSalary += p.salary;
if (p.age < youngestAge) youngestAge = p.age;
}
这个循环同时做了两件事:计算总薪资和找最年轻的人。虽然只遍历了一次,但代码的可读性很差。而且,如果后续要修改其中一个逻辑,很容易影响到另一个。
重构后,拆成两个循环:
let totalSalary = 0;
for (const p of people) {
totalSalary += p.salary;
}
let youngestAge = Infinity;
for (const p of people) {
if (p.age < youngestAge) youngestAge = p.age;
}
你可能会担心性能问题。嗯,这里我要说一句:大多数情况下,代码的可读性比那一次遍历的性能重要得多。除非你的数据量真的到了百万级别,否则别纠结。
我曾经踩过的坑: 有一次我为了“优化性能”,把三个循环合并成一个。结果三个月后,我自己都看不懂那个循环在干什么了。后来我花了半天时间才把它拆开。从那以后,我再也不在可读性上妥协了。
6.3 以管道取代循环(Replace Loop with Pipeline)
这个手法,说白了就是用 map、filter、reduce 这些集合操作,替代传统的 for 循环。我个人非常喜欢这种方式,因为它让数据的流向变得清晰。
看个例子,重构前:
const names = [];
for (const i of input) {
if (i.job === 'programmer') {
names.push(i.name);
}
}
重构后:
const names = input
.filter(i => i.job === 'programmer')
.map(i => i.name);
你看,数据从输入开始,经过过滤,再经过映射,最终得到结果。每一步都很清晰,而且可以单独测试。
核心原则: 管道操作让数据流变得显式。你不再需要手动管理循环变量和中间结果,只需要关注“数据怎么变”。
当然,不是所有循环都适合用管道取代。比如需要 break 或 continue 的场景,或者需要同时访问前后元素的场景,用管道反而会变得复杂。这时候,保留循环反而是更好的选择。
6.4 移除标记参数(Remove Flag Argument)
这个手法,我建议你重点掌握。标记参数,就是那种布尔值或者枚举值,用来控制函数内部走哪条分支的。
function bookConcert(aDate, isPremium) {
if (isPremium) {
// 高级会员逻辑
} else {
// 普通会员逻辑
}
}
调用的时候,你看到的是:
bookConcert(date, true);
bookConcert(date, false);
问题来了:true 和 false 是什么意思?你每次都得去看函数定义。而且,如果将来有第三种会员类型,这个函数会变得更复杂。
重构后,拆成两个函数:
function bookPremiumConcert(aDate) {
// 高级会员逻辑
}
function bookRegularConcert(aDate) {
// 普通会员逻辑
}
调用方也变得清晰了:
bookPremiumConcert(date);
bookRegularConcert(date);
我的建议: 如果你看到一个函数里有两个以上的标记参数,那基本可以确定这个函数需要拆分了。我曾经在一个遗留系统里见过一个函数有五个标记参数,那简直就是一场噩梦。
知识体系总览
下面这张图,我把今天讲的四种手法以及它们之间的关系梳理了一下。你可以把它当作一个快速参考。
总结一下
今天这四种手法,其实都指向同一个目标:让代码的意图更清晰。组合函数让数据和行为在一起;拆分循环让每个循环职责单一;管道让数据流变得显式;移除标记参数让调用方一目了然。
嗯,这些手法看起来简单,但真正用好需要刻意练习。我建议你从今天开始,每次 review 代码时,都问自己一个问题:这段代码,能不能用今天的手法改得更清晰?
一句话记住: 好的代码不是写出来的,是改出来的。重构,就是那个“改”的过程。