首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从"会写代码"到"写对代码":7 个让我顿悟的工程师思维模式

从"会写代码"到"写对代码":7 个让我顿悟的工程师思维模式

作者头像
前端达人
发布2026-06-12 13:56:53
发布2026-06-12 13:56:53
1580
举报
文章被收录于专栏:前端达人前端达人

代码跑起来,只是及格线。让下一个人读懂、改得动、出了问题能追查,才算真正写完了。

我刚工作那几年,一直以为高级工程师厉害,是因为他们懂得更多

更多框架、更多设计模式、更多算法技巧。于是我拼命学,掘金、GitHub、技术公众号,恨不得把所有库的源码都看一遍。

直到有一次,线上出了个 Bug,我被拉进一个有五年经验的老工程师的修复流程里。

他打开出问题的文件,三秒钟定位到了错误位置。

不是因为他更聪明。是因为那段代码是他写的——他刻意把错误藏得很浅

那一刻我才意识到:他和我的差距,不是知识量,是写代码时想的问题不一样

我在想"怎么让这个功能跑起来",他在想"这段代码出了问题,下一个人能不能在 3 分钟内找到原因"。

这篇文章,是我从几位老工程师身上观察到的 7 个思维模式。它们不绑定框架,不依赖语言,但会让你写出来的代码,在需求变更、线上告警、团队交接时,表现出完全不同的质量。

01 先清路障,再走主路

很多人写函数,像在挖隧道——一层一层往里钻。

先判断用户存不存在,再判断参数合不合法,再判断权限够不够,一路嵌套下去,等真正的业务逻辑终于出现,已经在第六层缩进里了。

代码语言:javascript
复制
用户输入
  └─ if (user)
       └─ if (input.email)
            └─ if (user.canEdit)
                 └─ 真正的业务逻辑  ← 读到这里眼睛已经累了

老工程师不这么写。他们先把所有"不该继续"的情况处理掉,剩下的才是主流程

代码语言:javascript
复制
async function updateUserProfile(userId: string, input: ProfileInput) {
const user = await getUser(userId);
if (!user) thrownew NotFoundError('用户不存在');

if (!input.email) thrownew ValidationError('邮箱不能为空');

if (!user.canEditProfile) thrownew ForbiddenError('无权修改资料');

// 到这里,所有异常已经被清空,这一行就是全部重点
return saveProfile(user.id, input);
}
代码语言:javascript
复制
// JavaScript 版本
asyncfunction updateUserProfile(userId, input) {
const user = await getUser(userId);
if (!user) thrownewError('用户不存在');
if (!input.email) thrownewError('邮箱不能为空');
if (!user.canEditProfile) thrownewError('无权修改资料');

return saveProfile(user.id, input);
}

看起来只是格式调整,实际上改变了读代码的方式

线上出问题时,你首先想的是"什么情况会导致这个函数走不下去",而不是"正常情况下会发生什么"。提前返回,就是把"失败路径"暴露在最显眼的地方,让排查变成一件顺序往下读的事,而不是在缩进迷宫里转圈。

核心思维:减少读代码时需要在脑子里维持的"上下文状态"。每多一层嵌套,读者就要多记住一个条件。把条件清完再写主逻辑,是对下一个人的礼貌。

02 命名要说清业务在干什么

变量名写 dataresultitemtemp,代码能跑,但它在撒谎。

它告诉你"这里有个东西",却不告诉你"这个东西是什么、为什么重要"。

代码语言:javascript
复制
// 这段代码在干什么?
const result = await getData(id);
if (result.status === 'active') {
  await process(result);
}

三个月后你回来看这段代码,getData 取的是什么?result.status === 'active' 代表什么业务含义?process 又做了什么事?

换个写法:

代码语言:javascript
复制
const subscription = await getSubscription(subscriptionId);
if (subscription.isBillable) {
  await chargeSubscription(subscription);
}

这段代码讲了一个完整的业务故事:取出订阅,如果可以计费,就扣款。

一行注释都不需要。

命名不只是"好看"的问题,它直接影响错误发生的概率。我见过一个线上故障,根因是一个叫 activeUsers 的变量,实际包含了"已停用但未删除"的用户。名字给了错误的保证,调用方没有防御,直接用出了问题。

名字越精准,错误的藏身之地就越少。

核心思维:命名应该表达业务意图,而不是数据结构。invoicesPendingPaymentunpaidList 更长,但长得有价值——它携带了别人不需要再去猜的信息。

03 给外部系统套上一层"翻译层"

第三方 API、支付网关、短信服务、外部数据库——这些你管不了。

它们会改字段名,会删接口,会返回你完全没预期的数据格式。如果你让这些外部数据的结构直接流进你的业务逻辑,就等于把一个你控制不了的人,拉进了你的内部架构。

代码语言:javascript
复制
// ❌ 外部结构直接泄漏进业务逻辑
const userName = response.data.user_name;
const isVip = response.data.member_level === 'VIP';
const expireAt = response.data.expire_ts * 1000; // 还要做单位转换

这段代码今天没问题。等第三方悄悄把 user_name 改成 userName,把 expire_ts 改成 expireTime,你才发现这种写法散落在十几个文件里。

老工程师会在边界处建一个"翻译层":

代码语言:javascript
复制
// ✅ 外部数据只在一处被消化
function mapMemberInfo(response: ThirdPartyMemberResponse): MemberInfo {
  return {
    name: response.data.user_name,
    isVip: response.data.member_level === 'VIP',
    expireAt: new Date(response.data.expire_ts * 1000),
  };
}

翻译层之外,整个系统只认识 MemberInfo,不知道也不在意第三方叫它什么。

代码语言:javascript
复制
第三方 API 响应
      │
      ▼
┌─────────────┐
│  mapMember  │  ← 唯一知道外部格式的地方
│  Info()     │
└──────┬──────┘
       │
       ▼
  内部 MemberInfo
  ┌──────────────────┐
  │  业务逻辑         │
  │  UI 组件          │  ← 只认识你自己定义的类型
  │  数据库写入       │
  └──────────────────┘

这个模式不只适用于第三方 API,同样适用于:

  • 数据库原始查询结果不要直接传给前端
  • HTTP 请求体不要直接塞进领域逻辑
  • 环境变量解析不要散落在各个文件里

核心思维:你控制不了的东西,就把它关在门口。让外部的混乱止步于边界层,不要让它成为你整个系统的语言。

04 让非法状态在代码里"没地方藏"

我曾经写过这样的类型定义:

代码语言:javascript
复制
type User = {
  id?: string;
  email?: string;
  role?: string;
  status?: string;
};

每个字段都可选,因为这样 TypeScript 不报错。

结果是:整个系统里,每一处用到 User 的代码都要自我防御——这个 id 存在吗?role 是预期的值吗?status 是什么意思?

老工程师看到这段代码直接说了一句话:"你这个类型允许了不该存在的状态。"

改法是把不同阶段、不同状态的对象,拆成不同的类型

代码语言:javascript
复制
// 未保存的草稿用户
type DraftUser = {
  email: string;
  role: 'admin' | 'member';
};

// 已入库的用户
type SavedUser = {
  id: string;
  email: string;
  role: 'admin' | 'member';
  status: 'active' | 'disabled';
};

这么一拆,"没有 id 的已保存用户"就不可能存在了——类型本身拒绝了它。

这个思维在业务建模里特别重要。订单不只是订单,它有待付款、已付款、已发货、已完成、已退款之分。支付不只是支付,它有待确认、已授权、已扣款、已失败之分。

代码语言:javascript
复制
type Payment =
  | { state: 'pending'; id: string }
  | { state: 'authorized'; id: string; authCode: string }
  | { state: 'captured'; id: string; receiptNo: string }
  | { state: 'failed'; id: string; failReason: string };

// 发送收据只接受"已扣款"状态的支付,编译期就能保证
function sendReceipt(payment: Extract<Payment, { state: 'captured' }>) {
  return emailReceipt(payment.receiptNo);
}

用 JavaScript 动态语言同样可以做到,在构造函数、工厂函数或校验层里做状态约束,效果一样。

核心思维:与其在每个调用方都加防御判断,不如从源头让"不合理的状态"无处落脚。类型是设计,不是装饰。

05 把"决策"和"执行"分开

你有没有写过这种函数:既判断条件,又发请求,又写数据库,又发邮件?

代码语言:javascript
复制
async function refundOrder(orderId: string) {
const order = await getOrder(orderId);

if (order.status !== 'paid') thrownewError('订单未支付');
if (order.refundedAt) thrownewError('已退款');
if (order.amount <= 0) thrownewError('金额异常');

await paymentGateway.refund(order.paymentId);
await markOrderRefunded(order.id);
await sendRefundEmail(order.userId);
}

这个函数问题不大——直到你需要给退款逻辑写单元测试。

你会发现,要测"订单已退款应该报错"这一条规则,你得 mock 支付网关、mock 数据库写入、mock 邮件发送。测一个纯逻辑判断,要搭这么大的架子。

慢慢地,测试越写越累,团队干脆不写了。

老工程师会先把决策抽成纯函数

代码语言:javascript
复制
function getRefundEligibility(order: Order): RefundEligibility {
  if (order.status !== 'paid') return { allowed: false, reason: '订单未支付' };
  if (order.refundedAt) return { allowed: false, reason: '已退款,请勿重复操作' };
  if (order.amount <= 0) return { allowed: false, reason: '退款金额异常' };
  return { allowed: true };
}
代码语言:javascript
复制
决策层(纯函数,无副作用)
┌──────────────────────┐
│  getRefundEligibility│  ← 只做判断,不碰外部世界
│  (order) → allowed?  │  ← 随便测,不需要 mock 任何东西
└──────────┬───────────┘
           │ allowed: true
           ▼
执行层(有副作用)
┌──────────────────────┐
│  paymentGateway      │
│  markOrderRefunded   │  ← 只在决策通过后才触发
│  sendRefundEmail     │
└──────────────────────┘

执行层变得干净:

代码语言:javascript
复制
async function refundOrder(orderId: string) {
const order = await getOrder(orderId);
const eligibility = getRefundEligibility(order);

if (!eligibility.allowed) thrownew ValidationError(eligibility.reason);

await paymentGateway.refund(order.paymentId);
await markOrderRefunded(order.id);
await sendRefundEmail(order.userId);
}

业务规则现在是可见的、可测试的、可复用的。

权限判断、定价计算、通知触发条件、工作流流转——这些"决策"都适合用这个模式拆出来。

核心思维:把业务规则藏在有副作用的函数里,等于把规则变成了谜题。把决策提出来,规则才是一等公民。

06 错误信息是写给人看的,不是写给控制台看的

你见过多少次这样的报错:

Something went wrong

有什么用?

  • 用户不知道出了什么问题
  • 客服不知道怎么帮用户
  • 开发不知道哪段代码出的问题
  • 日志系统不知道这条错误属于哪个业务流程

老工程师写错误,像写给三类人看的:接口调用方、日志排查者、最终用户。

代码语言:javascript
复制
// ❌ 没有诊断价值的错误
throw new Error('操作失败');

// ✅ 有机器可读编码 + 人类可读描述 + 上下文信息
throw new BusinessError({
  code: 'ORDER_ALREADY_REFUNDED',
  message: '该订单已完成退款,如有疑问请联系客服',
  details: { orderId, refundedAt: order.refundedAt },
  requestId,
});

对于 API 接口,前端最怕的是靠解析错误文本来判断业务状态:

代码语言:javascript
复制
// ❌ 前端被迫解析字符串,脆得一碰就断
if (error.message.includes('已退款')) {
  showDuplicateRefundTip();
}

// ✅ 前端通过 code 判断,稳定可靠
if (error.code === 'ORDER_ALREADY_REFUNDED') {
  showDuplicateRefundTip();
}

日志也一样,不是 console.log('退款失败') 了事,而是带上足够的上下文:

代码语言:javascript
复制
logger.warn('退款资格校验未通过', {
  orderId,
  userId,
  reason: eligibility.reason,
  requestId,
});

注意:日志里绝对不能出现密码、支付卡号、短信验证码、用户身份证号这类敏感信息。有用的上下文是 ID、状态、流程节点,不是用户隐私。

核心思维:错误信息是系统对外的沟通语言。写得越模糊,出问题时付出的代价越大。

07 优化给 Code Review 看的 diff,不只是给 Demo 看的效果

功能在本地跑通了,不等于这个改动做完了。

合并之后的代码会被 Review、被测试、可能被回滚、一定会被将来的某个人阅读。

我见过很多这样的 PR:

代码语言:javascript
复制
feat: 完善支付流程

- 重构发票服务
- 重命名支付字段
- 修改退款逻辑
- 更新 Dashboard UI
- 新增 Webhook 处理
- 修复账户状态 Bug
- 更新单元测试

这不是一个 PR,这是一个黑盒。

Reviewer 不知道哪些改动是功能必要的,哪些是顺手重构的。出了问题不知道 revert 哪一个。测试用例覆盖的是哪个改动的行为?

老工程师会把这个拆成:

代码语言:javascript
复制
PR 1:重命名支付字段(纯重构,无行为变更)
PR 2:提取退款资格校验函数(+测试)
PR 3:将退款资格校验接入支付流程
PR 4:更新 Dashboard 展示退款原因
PR 5:新增 Webhook 重试机制

每个 PR 只讲一件事。

代码语言:javascript
复制
一个好的 PR diff 应该能回答三个问题:
┌──────────────────────────────────┐
│  改了什么?(What changed)       │
│  为什么改?(Why it changed)     │
│  现在行为有什么不同?(New behavior)│
└──────────────────────────────────┘

这种习惯在平时看起来"麻烦",在出线上问题的时候会救你的命。小而聚焦的 diff,是高压时刻团队最宝贵的礼物。

核心思维:代码不是写给机器的,是写给人的。可 Review、可回滚、可追溯的改动,才是真正"交付完成"的改动。

写在最后:这 7 个模式的底层只有一句话

把这 7 个模式摊开来:

提前返回、精准命名、边界封装、合法状态建模、决策与执行分离、有意义的错误、干净的 diff。

它们看起来各讲各的,但底层其实只有一件事:

减少下一个人需要猜测的东西。

猜变量名是什么意思,猜函数会不会有副作用,猜错误是哪里抛出来的,猜这个状态算不算合法,猜这个 PR 改了什么……

每一次猜测,都是认知成本。认知成本堆多了,就变成"不敢动这段代码"、"先测试一遍再说"、"这个功能不知道影不影响那里"。

一个让人频繁猜测的代码库,会让整个团队陷入低效的焦虑状态。不是因为团队不行,是因为代码一直在消耗信任。

会写代码,是让功能跑起来。

写对代码,是让下一个人不需要猜。

💬 你最近有没有遇到"让你猜了很久"的代码? 是一个神秘变量名、一个嵌套了七层的 if、还是一个不知道会触发什么副作用的函数? 评论区分享你的「翻车现场」,最有共鸣的我会在下期文章里引用,顺手帮你分析怎么改 👀

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 01 先清路障,再走主路
  • 02 命名要说清业务在干什么
  • 03 给外部系统套上一层"翻译层"
  • 04 让非法状态在代码里"没地方藏"
  • 05 把"决策"和"执行"分开
  • 06 错误信息是写给人看的,不是写给控制台看的
  • 07 优化给 Code Review 看的 diff,不只是给 Demo 看的效果
  • 写在最后:这 7 个模式的底层只有一句话
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档