笔记
← 返回《重构-改善既有代码的设计》系列目录 NOTES

10|为什么函数应该短?——过长函数的真正代价

函数长一点,不是能把所有逻辑一次看完吗?福勒为什么偏偏坚持函数要短?

10|为什么函数应该短?——过长函数的真正代价.md 更新于 2026-09-23 ~14 分钟阅读

一、先说结论

要点

福勒坚持函数要短,不是因为「行数越少越高级」,而是因为长函数的真正问题在于「语义层次」被搅在了一起。

一个长函数里,往往同时装着一件事的高层意图、中层步骤和底层细节。读它的人必须一边处理「这段在干什么」,一边处理「这行具体怎么实现」,脑子被迫同时记住十几件事。这就是它难读的根源,而不是「行数太多」本身。

短函数的解法,是一次只讲清楚一件事,并让名字代替注释。福勒最核心的手段是提炼函数(Extract Function):把一段逻辑抽出来,起一个能说明意图的名字。他还给过一条实用判据:如果一个函数需要注释来解释它「在做什么」,那就该考虑把这段提炼出去,让注释变成函数名。

但要注意分寸:函数短不是越短越好,拆得太碎同样有害。


二、我们先理解一个问题

看下面这段伪代码。它是一个「提交订单」的函数,大约一百多行,大致结构是这样的:

submitOrder():
  1. 校验用户是否登录、是否有权限、是否被风控拦截
  2. 校验商品是否上架、库存是否充足、限购数量是否超限
  3. 计算原价、优惠、运费、税费、最终金额
  4. 扣减库存、生成订单、创建支付单、发送通知
  5. 写日志、埋点、更新缓存

这段代码能跑,而且逻辑都是对的。

有人会说:「长函数不是挺好的吗?所有逻辑都在一个地方,从上到下看一遍就全懂了,不用在多个函数之间跳来跳去。」

这个说法听起来很有道理,它抓住了「局部性」这个优点。但它忽略了一件事:当你从上往下读这一百多行时,你的大脑正在做一件非常费力的工作——它必须记住前面所有的细节,才能理解后面的一行。

于是问题变成:


三、作者到底提出了什么观点?

福勒关于长函数的观点,可以拆成三层。

第一层:问题不在长度,在「语义层次混乱」。

一个函数里,如果并排出现「校验用户权限」和 if (user.level > 3 && user.flags.contains("A")),那问题不在于它有多少行,而在于这两个东西不是同一个抽象层次。前者在说「做什么」,后者在说「怎么做」。把它们塞在一起,读者就得在两种思维模式之间反复切换。

第二层:短函数的价值,是用名字替代注释。

福勒有一个很实用的判断方式:如果你需要写一段注释来解释某几行代码在干什么,那就干脆把那几行提炼成一个函数,用函数名当注释。 这样注释就变成了代码的一部分——而且它不会像注释那样过期。注释可能忘记更新,函数名却必须跟着代码改。

第三层:短函数更易测试、复用和组合。

函数短、只做一件事,就更容易被单独测试(输入明确、职责单一),也更容易在不同地方被复用来拼装更大的逻辑。长函数则往往只能整体测试、整体复用,一旦要拆借其中一部分,就得连着一堆无关逻辑一起搬走。

福勒用的核心工具是提炼函数(Extract Function)——这是他在书里最基本、也最高频的重构手法之一。


四、把这个观点翻译成人话

用一篇不分段的文章来理解,最合适。

想象你拿到一篇文章,它整整一页,没有任何分段。所有内容从第一行一直排到最后一行。

奇怪的是,这篇文章其实写得不错,句子也通顺,内容也有条理。但你读起来就是很累,读到最后已经忘了开头讲了什么。

为什么?因为段落就是文章的「语义层次」。分段做的事,是告诉你:这一块在讲一件事,下一块在讲另一件事。没有段落,读者就失去了这个导航,只能自己一边读一边在脑子里重新切分。

好的分段文章则完全不同:你扫一眼小标题,就知道每段在讲什么;需要细节时再进去看,不需要时可以直接跳过。段落的标题,就是函数的「名字」。

长函数就是那篇不分段的文章。短函数则像结构清晰的报告:每一节只讲一件事,标题说明了它讲什么。你甚至不需要读完每一节,也能把握整体结构。

福勒说的「函数应该短」,本质就是这句话:


五、为什么会这样?

把这条逻辑整理成推理链:

graph TD
    A[一段逻辑同时包含高层意图与底层细节] --> B[所有内容挤在同一个函数里]
    B --> C[读者必须同时记住多个层次的细节]
    C --> D[理解成本急剧上升]
    D --> E[改一处时不敢确定影响范围]
    E --> F[只能靠注释和口头经验来补]
    F --> G[注释会过期, 经验会流失]
    G --> D
    B --> H[提炼函数: 抽出子逻辑并命名]
    H --> I[函数名承担了原来注释的职责]
    I --> J[每个函数只讲一件事, 层次清晰]
    J --> K[更易理解, 测试, 复用, 组合]
    K --> L[修改时定位成本下降]

关系示意图

图里最关键的是 C 这一步。

长函数之所以费脑,是因为人一次能同时持有的「活跃信息」是有限的。一个函数里层次越多,读者需要同时抓住的上下文就越多,一旦超过容量,理解就会失败——这不是能力问题,是认知规律。

而提炼函数做的事,是用命名把信息压缩。当一段底层细节被折叠成一个名字叫「校验用户权限」的函数后,读者在高层阅读时,只需要看这个名字,不必关心里面怎么实现;只有真正需要时,才展开进去看细节。这就是所谓的「分层阅读」:名字负责压缩复杂度。


六、举一个现实生活中的例子

用阅读一份技术文档和一段代码做对比,会更贴近开发者。

假设你接手一个项目,要理解「优惠券是怎么算进去的」。你打开代码,看到这样一个方法:

public BigDecimal calculateTotal(Order order) {
    // ... 20 行校验 ...
    // ... 15 行库存判断 ...
    // ... 40 行价格与优惠计算 ...
    // ... 25 行下单、通知、日志 ...
}

一百多行读下来,你大概能勉强明白它在干什么,但你也说不清「优惠券到底在哪一步生效」——因为它被埋在一大堆别的东西中间。

现在换一个版本:

public BigDecimal calculateTotal(Order order) {
    validateOrder(order);
    checkStock(order);
    BigDecimal price = calculatePrice(order);
    BigDecimal discount = applyCoupon(order, price);
    return applyShipping(price, discount);
}

同样是十秒阅读,但这个版本让你一眼就看到了整体流程:校验、查库存、算价、用券、加运费。你甚至不需要看每个方法的实现,就已经理解了主逻辑。想看优惠券的具体规则时,再点进 applyCoupon 就好。

这就是短函数带来的核心变化:高层读结构,低层读细节,两者不再互相干扰。

这和文章分段是同一个道理。你读一份报告,先看目录和小标题,就掌握了全文骨架;要深究某一节,再翻进去。没人要求你为了理解文章,把每一段的每个字都同时记在脑子里。

同样地,函数过短的极端也要提防。如果一个函数只是简单转发一次调用,名字也没有增加任何信息量,那它就只是增加了「跳来跳去」的负担。短是为了让层次清晰,不是为了把代码切碎。


七、回到书中重新理解

现在再回看福勒那句「函数应该短」,就不会把它误解成一种行数崇拜了。

他真正关心的是读者的理解成本。在书里,提炼函数被放在最基础的位置,几乎所有其他重构手法最后都会用到它——因为「把一段有意义的逻辑抽出来并命名」,是让代码变得可读的通用动作。

福勒还反复强调命名的重要性。他并不认为「短」本身能带来可读性,「短 + 有意义的名字」才可以。一个只有三行、却叫 doStuff() 的函数,不会让任何人更容易理解;一个被抽出来、名字准确表达意图的函数,才真正把复杂度压缩掉了。

所以他反对的从来不是「长」,而是「一段代码同时承担了多个抽象层次,且没有名字来区分它们」。理解了这一点,你就明白了为什么这一章的标题是「长函数」,而解药却主要是「命名」和「提炼」。


八、这个观点背后还有什么?

第一,它和「注释」那一条坏味道是连着的。

福勒说过一句容易引起误解的话:注释有时是坏味道。他的意思并不是「不要写注释」,而是:当一个注释在解释「代码在干什么」时,它往往是在替一段表达不清的代码打掩护。 如果能用提炼函数加命名,让代码自己说清楚,那比注释更可靠——因为注释会过期,函数名不会。

第二,它和「单一职责」在函数层面的投影是同一个东西。

一个函数只做一件事,其实是把「职责划分」缩小到了函数粒度。函数太长,通常意味着它承担了太多职责;把它拆短,就是在恢复职责的边界。这一点,在讲「过大的类」时会以更大的尺度再次出现。

第三,命名是一种「知识压缩」,它把口头经验固化了下来。

老手读长代码时,能凭经验迅速分辨层次;但这份能力在团队成员之间无法传递。短函数加上准确命名,把这种「只有老手才有的分层能力」写进了代码里,让新人也读得懂。这是它对团队协作的真正价值。


九、这个观点有问题吗?

十、今天再看这个观点

(以下为现代延伸,非福勒原文)

长函数这个话题,在工具发达之后,姿态发生了一点变化。

一方面,IDE 让提炼函数变成了几乎零成本的操作。选中一段代码,一键提取、自动生成参数、自动替换调用点。当年需要手工搬运、小心验证的重构动作,现在几秒钟就能完成。这意味着「把长函数拆短」的执行门槛,比福勒写书时低得多。

另一方面,「短」这件事本身可以被工具自动度量。圈复杂度、函数行数、嵌套深度都能被静态检查直接报出来。于是有些团队把「函数不超过 N 行」写进了代码规范,交给流水线把关。

但这也带来了反向的风险:当「短」变成一条可自动检查的指标时,它很容易被异化成数字游戏。 为了压行数而拆函数、为了过检查而机械提炼,都会产生那种「很短、但更难懂」的代码。指标能衡量长度,衡量不了清晰度。

还有一层现代延伸,和 AI 有关。AI 生成代码时,天然倾向于产出长函数——它把用户描述的所有步骤一次性铺进一个方法里,读起来像是把需求文档逐句翻译成了代码。这种代码往往「能跑、层次混乱」,恰恰是最需要提炼函数的那一类。

于是今天真正的分工可能是这样:工具负责发现「这里太长了」,AI 负责执行「把它拆开」,而人负责判断「拆成什么名字、按什么边界拆、拆完是不是真的更清楚了」。 最后那一问——「这个提炼有没有降低理解成本」——始终是人的工作。


十一、这一篇真正应该记住什么?

  1. 长函数的问题不在「长」,在「语义层次混乱」。 高层意图和底层细节搅在一起,读者要同时记住太多东西,这才是难点。

  2. 短函数的本质是用名字替代注释。 把一段逻辑提炼出来并起个说明意图的名字,比写一段会过期的注释更可靠。

  3. 提炼函数是最核心的动作。 如果一个函数需要注释来解释它在做什么,就考虑把那段逻辑提炼出去。

  4. 短函数更容易测试、复用和组合。 职责单一的函数,输入明确、边界清晰,是拼装更复杂逻辑的更好积木。

  5. 短是手段,不是目标。 拆得太碎、名字空洞的代码同样难读;判断标准始终是「理解成本有没有真的下降」。


十二、留一个问题

我们已经看到,一个函数太长的典型症状,是「把太多不同层次的东西塞进了一件事里」。

那么,如果把这个尺度放大到一个类呢?

一个类如果什么都管——下单、退款、通知、报表、对账全在里面——它会变成什么样子?为什么这种类在维护时如此令人头疼?而那些参数多到写不下调用语句的函数,又是怎么一步步长成那样的?

下一篇,我们就来看看复杂度是如何在更大的尺度上堆成山的:上帝类与过长参数列表。