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)

这个手法,说白了就是用 mapfilterreduce 这些集合操作,替代传统的 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);

问题来了:truefalse 是什么意思?你每次都得去看函数定义。而且,如果将来有第三种会员类型,这个函数会变得更复杂。

重构后,拆成两个函数:

function bookPremiumConcert(aDate) {
  // 高级会员逻辑
}

function bookRegularConcert(aDate) {
  // 普通会员逻辑
}

调用方也变得清晰了:

bookPremiumConcert(date);
bookRegularConcert(date);

我的建议: 如果你看到一个函数里有两个以上的标记参数,那基本可以确定这个函数需要拆分了。我曾经在一个遗留系统里见过一个函数有五个标记参数,那简直就是一场噩梦。

知识体系总览

下面这张图,我把今天讲的四种手法以及它们之间的关系梳理了一下。你可以把它当作一个快速参考。

基本重构手法(三) 组合函数 将操作同一数据的函数收拢 封装成类或转换函数 拆分循环 一个循环只做一件事 提升可读性和可维护性 以管道取代循环 用 map/filter/reduce 替代传统 for 循环 移除标记参数 用多个函数替代布尔/枚举参数 让调用意图更清晰 目标:让代码更清晰、更易维护

总结一下

今天这四种手法,其实都指向同一个目标:让代码的意图更清晰。组合函数让数据和行为在一起;拆分循环让每个循环职责单一;管道让数据流变得显式;移除标记参数让调用方一目了然。

嗯,这些手法看起来简单,但真正用好需要刻意练习。我建议你从今天开始,每次 review 代码时,都问自己一个问题:这段代码,能不能用今天的手法改得更清晰?

一句话记住: 好的代码不是写出来的,是改出来的。重构,就是那个“改”的过程。

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