首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >你的代码不是写得烂,是「写得急」——9 个补救动作照着改

你的代码不是写得烂,是「写得急」——9 个补救动作照着改

作者头像
前端达人
发布2026-07-20 22:00:25
发布2026-07-20 22:00:25
100
举报
文章被收录于专栏:前端达人前端达人

先还原一个咱们都经历过的场景。

周四下午,产品在群里 @你:「这个需求今晚能上不?运营等着明天发券。」你看了眼,逻辑不复杂,加个判断的事儿。于是你复制了隔壁一段相似的代码,改了两个值,变量懒得重命名,datatempresult 先顶上,心里默念一句「回头再收拾」,提交,上线,收工。

回头,永远没来。

三个月后的凌晨两点,你的手机响了。线上支付在报错,运营群已经炸了,值班的同事——可能是你,也可能是接手你项目的新人——打开那个函数:180 行,变量还是 datatemp,每个分支都在改同一个共享状态。代码是能跑的,但没人知道它为什么能跑,更没人敢动它。

这就是难读代码真正的代价:它不在你写下的那一刻爆炸,它在别人要快速、安全、又没有你脑子里那点上下文的情况下去改它的那一刻爆炸。

而且说句公道话——大多数难读的代码,真不是烂程序员写的,是被 deadline 追着跑的好程序员写的。所以这篇不批判谁,只给补救。我把 review 时最常打回的问题,整理成 9 个能直接照着改的动作,每个都配一个咱们熟悉的业务场景。你可以对着自己手上的项目,一条条过。

补救 1:让代码自己说「这是激活用户」

场景:用户中心要筛「可下单的活跃用户」,发券、发短信都调它。

当时你三分钟写完了这行:

代码语言:javascript
复制
// ❌ 三个月后接手的人:s 是啥?1 为什么是激活?b 又是啥?
const x = users.filter((u) => u.s === 1 && u.r !== 'b');

问题不在这行短,而在于读的人得去翻类型定义、翻数据库枚举、翻你三个月前的提交,才敢确认 s === 1 是「激活」、r === 'b' 是「封禁」。营销活动要复用这段逻辑时,没人敢直接拿去用——万一 1 不是激活呢?

改法很便宜,就是把业务规则从你脑子里挪进代码里:

代码语言:javascript
复制
// ✅ 谁读都不用猜
const activeCustomers = users.filter(
  (user) => user.isActive && user.role !== UserRole.Banned,
);

编译器根本不在乎变量叫 x 还是 activeCustomers。但 code review 的同事、排障的值班、下个月接手活动的新人,全都在乎。好命名的本质,是替后面每一个读代码的人,省掉一连串「这是啥意思」的提问。

补救 2:别让「炫技」拖住半夜排障的人

场景:订单列表要按类型分组,你想秀一把,一行搞定。

代码语言:javascript
复制
// ❌ 一行塞了 5 个语言细节:表达式内赋值、??=、变异、逗号运算符、reduce
const result = itemList.reduce(
  (a, x) => ((a[x.type] ??= []).push(x), a),
  {},
);

写的时候很爽,十行压成一行,同事路过还夸一句「牛」。但真到线上订单分组出问题、有人半夜来 debug 时,他得同时在脑子里跑通这五个细节,才敢下手。这一行省下的时间,全加倍还给了排障的人。

换成朴素写法,多几行,但谁都能改:

代码语言:javascript
复制
// ✅ 无聊,但一眼看懂
const ordersByType = {};

for (const order of orderList) {
  if (!ordersByType[order.type]) {
    ordersByType[order.type] = [];
  }
  ordersByType[order.type].push(order);
}

不是说 map/filter/reduce 就有罪——团队都熟的写法,紧凑照样好读。坏就坏在「显得高级」压过了「让人看懂」。生产代码不是炫技比赛,它是全组共用的操作系统。别让接盘的人先破译你的智商,才能修那个 bug。

补救 3:那个 200 行的「下单函数」,该拆了

场景placeOrder 一个函数从头干到尾——校验、查权限、算价、用券、扣款、发邮件、写日志、拼返回。

代码语言:javascript
复制
// ❌ 加了注释「看起来」有条理,架构还是一团麻
asyncfunction placeOrder(req, res) {
// 校验请求
// 查用户权限
// 算商品价格
// 应用优惠券
// 调支付扣款
// 保存订单
// 发确认邮件
// 写审计日志
// 返回结果
}

它的问题不在长,在于背了太多「要改的理由」:支付一挂,你得读懂整个下单流程;双十一改价格策略,改动就贴着支付代码,一不小心动到扣款;哪天邮件要改异步,同一个函数又得动。每一次需求变更,都在同一个火药桶上点火。

改法不是机械拆成一堆两行小函数,而是按有意义的业务决策来分

代码语言:javascript
复制
// ✅ 主流程读起来像一句人话
const order = await buildOrder(command);      // 校验+算价+用券,都在这后面
await paymentService.charge(order.payment);   // 支付只管支付
await orderRepository.save(order);
await orderEvents.publishCreated(order);       // 发邮件/写日志走事件,解耦

细节还在,只是躲到了「能解释它为什么存在」的名字后面。以后价格逻辑要改,你只进 buildOrder;支付出问题,你只看 paymentService。小函数在暴露关键概念时有用;但每两行就抽个 handleStuff(),那是噪音。目标不是更少的代码,是每个单元背更少的责任

补救 4:注释别翻译代码,要留「当时为什么这么干」

场景:对接第三方支付的 webhook 回调。

先看一种最没用的注释——把代码用中文又念一遍:

代码语言:javascript
复制
// ❌ 检查用户是否激活  ← 这句等于没说
if (user.status === 1) {

它不如直接把代码改得不用注释:

代码语言:javascript
复制
// ✅
if (user.isActive) {

但真正保命的注释,是记下代码本身表达不了的决定

代码语言:javascript
复制
// 支付方在网络抖动时会重发同一个 webhook,
// 处理前先用事件 ID 去重,否则用户会被重复扣款。
if (await hasProcessed(event.id)) return;

这条注释拦住的是一场事故。没有它,某个热心同事下次「优化」时,一眼觉得这个去重判断多余,删了——然后大促当天,重发的 webhook 让一批用户被扣了两次款,你又得凌晨爬起来。

对接外部服务、历史遗留兼容、反常的性能取舍、乍看不合理其实有原因的业务规则——这些地方的注释最值钱。记住:代码解释「做了什么」,注释解释「为什么非这么做不可」。

补救 5:调用处一排 true, false,鬼才看得懂

场景:生成对账报表,函数支持含税、导 PDF、发邮件等一堆开关。

代码语言:javascript
复制
// ❌ true 是含税?导 PDF?发邮件?用缓存?得翻定义数参数
generateReport(data, true, false);

半年后运营提需求:「报表能不能不发邮件」,接手的人盯着这行 true, false 完全不敢动,生怕改错一个位置把含税关了。多个位置参数是同一个病,加个字段就更乱:

代码语言:javascript
复制
// ❌ 含义全靠位置
createUser(name, email, role, true, false, null);

给它们起名,决策就摆在了调用处:

代码语言:javascript
复制
// ✅ 一眼看懂每个开关是干嘛的
generateReport(data, {
includeTax: true,
sendEmail: false,
});

createUser({
  name,
  email,
  role,
isVerified: true,
sendWelcomeEmail: false,
});

不是每个函数都要上对象参数——add(2, 3) 挺好。当参数代表配置、业务开关、或多个同类型的值时,再上命名选项。一次函数调用,光看调用处就该读懂决策,不用打开第二个文件去对照。

补救 6:会员权限判断,别再套四层 if

场景:判断用户能不能进会员专享页——要登录、要激活、要有订阅、订阅没过期。

代码语言:javascript
复制
// ❌ 读到最里层,脑子里还压着四个条件
if (user) {
  if (user.isActive) {
    if (user.subscription) {
      if (!user.subscription.expired) {
        return showDashboard(user);
      }
    }
  }
}

这种金字塔,加需求时最痛——产品说「过期的也让进,但弹个续费引导」,你得数着大括号找该在哪插分支,还容易插错层。用卫语句把非法情况在开头挡掉,主流程立刻就平了:

代码语言:javascript
复制
// ✅ 无效情况早退,还能给出具体理由
if (!user) {
thrownew NotFoundException('用户不存在');
}
if (!user.isActive) {
thrownew ForbiddenException('用户已被禁用');
}
if (!user.subscription || user.subscription.expired) {
return showUpgradePage(user);   // 过期→引导续费,理由明确
}
return showDashboard(user);

一张图看这个「拍平」,左边越读越背状态,右边逐个拦截、每步都有明确理由:

代码语言:javascript
复制
嵌套写法                          卫语句写法
─────────                        ──────────
if (user) {                       if (!user)   → 抛「不存在」
  if (isActive) {                 if (!active) → 抛「被禁用」
    if (subscription) {           if (过期)    → 跳续费页
      if (!expired) {             ───────────────────────
        showDashboard()           return showDashboard()
      }                           ← happy path 就一行,是平的
    }
  }
}
读到底:脑中挂 4 个条件 🤯        读到底:0 负担 😌

卫语句不是信仰,状态流转特别复杂时早返回太多也会乱。它最擅长的是在函数开头挡掉非法入参、缺失数据、越权、不支持的状态。一句话:读的人不该为了看懂最后一行,先在脑子里装下半个函数。

补救 7:一个团队,别让每个人发明自己的方言

场景:三个人分头写三个 service,风格各飞各的。

一个接口报错抛异常,另一个返回 null,第三个返回 { success: false };用户表里一会儿 createdAt、一会儿 created_at;同一个「查用户」操作,这边 getUser、那边 fetchUser、还有 loadUserfindUser

每处差异都小,加一起,就逼着新人一个文件一个文件重新学这套系统——每读一个模块都要先问「这个返回啥?出错抛还是不抛?」。可读的代码,是可预测的代码。

约定好:仓储方法要么返回实体、要么抛已知错误,就全程守住;API 返回遵守同一份契约,别让某个接口自创形状;命名风格统一,就别中途换词。这不是要把烂约定供着——坏模式该改,但要有意、且协调好地改。一个代码库里两套打架的约定,往往比一套不完美的约定更折磨人。名字具体叫什么不重要,大家一致才重要:让人能靠读懂熟悉的代码,去预测没读过的代码。

补救 8:catch (e) { console.log(e) } 说的是不是你

场景:下单里调支付,包了个 try-catch,「先把错误吞了别让页面崩」。

代码语言:javascript
复制
// ❌ 藏住了失败,调用方拿不到任何信号
try {
  await processPayment(order);
} catch (error) {
  console.log(error);
}

于是真出事那天,你面对的是这些问题,一个都答不上来:支付是在支付方接受之前挂的,还是之后?这单会不会重试?订单现在是不是还挂着?日志里有订单号能让我捞出来吗?——啥都没有,只有一行漂在控制台里、早被冲走的 error。

给失败一个结构,让它和成功路径一样「是设计出来的」:

代码语言:javascript
复制
// ✅ 出事时,现场留得下线索
try {
  await paymentService.charge(order);
} catch (error) {
  logger.error('支付扣款失败', {
    orderId: order.id,
    customerId: order.customerId,
    error,
  });
  throw new PaymentFailedError(order.id);   // 往上抛明确的错误,让调用方决定重试还是回滚
}

可读的错误处理,告诉人:什么会失败、谁负责兜底、出事时现场会留下什么。「系统繁忙请稍后再试」糊弄用户还行,对凌晨排障的人毫无用处。内部错误该带上稳定错误码、请求 ID、订单/用户标识和上下文——但别把密钥、token 也打进日志。一个只解释成功的系统,是没法被理解的。

补救 9:测试要写「行为」,不是写「调了哪个方法」

场景:还是那个重发 webhook 的幂等逻辑,你想加个测试锁住它。

代码语言:javascript
复制
// ✅ 这条测试在替你「说明」:webhook 处理必须幂等
it('同一个支付回调不会给用户扣两次款', async () => {
  await handlePaymentWebhook(event);
  await handlePaymentWebhook(event);   // 故意重发一次
  expect(paymentProvider.charge).toHaveBeenCalledTimes(1);
});

半年后有人来改这块逻辑,一看测试名就懂了红线在哪——「哦,这里不能重复扣款」。对比一条只盯实现细节的弱测试:

代码语言:javascript
复制
// ❌ 只证明「方法被调了」,没说清「为什么这行为重要」
expect(repository.findById).toHaveBeenCalled();
expect(repository.save).toHaveBeenCalled();

它保护不了任何业务约束,重构一动就红,还看不出到底哪儿错了。好测试用有意义的名字、把数据摆清楚、盯可观测的行为,把那些容易从记忆里蒸发的决定钉死。一条好测试,能让人比读完整个 service 更快看懂这个功能——它就是一份能跑起来的说明书。

说到底:可读,是团队的事,不是个人品味

一个人救不活整个代码库。可读的系统靠共识:命名约定、格式化规则、review 习惯、错误契约、目录结构、测试范式。

Code review 该问的,不止「这能跑吗」,而是——不看需求单能读懂意图吗?名字够具体吗?这函数是不是揽太多活了?失败路径看得见吗?新人知道该去哪改吗?这层抽象是在降复杂度,还是在藏复杂度?

也别走极端。生产团队真有 deadline,有时正确的选择就是先发一版清楚但略啰嗦的实现,而不是为一个优雅抽象拖住上线。回到开头那句——你的代码不是写得烂,是写得急。急没关系,但欠下的账,找个不那么急的下午,照着上面 9 条补回来。

好代码不证明你当年多聪明。它让下一个人不用把你从被窝里捞起来,就能改。写那种在你的记忆、你的需求单、你的口头解释都消失之后,还能活下去的代码。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 补救 1:让代码自己说「这是激活用户」
  • 补救 2:别让「炫技」拖住半夜排障的人
  • 补救 3:那个 200 行的「下单函数」,该拆了
  • 补救 4:注释别翻译代码,要留「当时为什么这么干」
  • 补救 5:调用处一排 true, false,鬼才看得懂
  • 补救 6:会员权限判断,别再套四层 if
  • 补救 7:一个团队,别让每个人发明自己的方言
  • 补救 8:catch (e) { console.log(e) } 说的是不是你
  • 补救 9:测试要写「行为」,不是写「调了哪个方法」
  • 说到底:可读,是团队的事,不是个人品味
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档