🏆 凌云拓界/雪豹同志
开放原子开源基金会·开源贡献之星
🤖 人工智能应用 |
🖥️ 前端开发 |
🐍 Python应用 |
🟢 Nodejs落地
引言:为什么聊“形成过程”?
先问一个问题。
你有没有遇到过这样的场景:一个新功能要开发,你花了一天时间把代码写完,跑通了,提测了,然后觉得“完事了”。可过两天,测试同学提了一个bug,你打开自己写的代码,看了半天,心想:“这行代码是干嘛的?我当时为什么要这么写?”
或者更糟糕的场景:你接手了一个同事留下的项目。打开第一个文件,3000行。打开第二个文件,同样3000行。变量名是a、b、tmp,没有注释,没有类型定义。你想改一个按钮的颜色,找了两个小时,没找到那个按钮的样式写在哪里。这时候你心里冒出的那句话,可能不太适合写出来。
这两个场景,其实指向同一个问题:代码能跑,和代码能维护,是两件完全不同的事。
学校里教的是前者——怎么让代码跑起来,怎么解决问题,怎么在限定时间内拿到分数。但职场需要的是后者——怎么让代码在三个月后、三年后,还能被人看懂、被人修改、被人接手。
这种能力,我们叫它工程思维。
但工程思维不是一门课,不是一本书,不是看完就能学会的“知识点”。它是一种在实践中慢慢长出来的东西。它的形成过程,往往伴随着踩坑、背锅、加班、被怼——然后某一天,你突然发现自己和以前不一样了。
这篇文章想做的,就是还原这个过程。不是告诉你“工程思维是什么”,也不是给你列“10条原则让你立刻成为高手”,而是试着回答一个问题:
一个开发者的思维,到底是怎么在实践中,一步步发生变化的?

第一部分:起点——大多数人的思维原点
要理解工程思维是怎么形成的,得先知道大多数人是从哪里出发的。
这个起点,可以统称为学科思维。
什么叫学科思维?就是你在学校、在竞赛、在培训课程里被训练出来的那套思维模式。它有以下几个特征:
🎯 目标:找正确答案
在学校里,每一道题都有标准答案。你写对了,得分;写错了,扣分。这种二元对立的评价体系,会潜移默化地塑造一种认知:代码的世界里,存在一个“正确”的解法。
于是刚入职场的新人,常常会问这样的问题:
- “这个功能的最佳实践是什么?”
- “用Vue还是React更好?”
- “这种场景下,应该用哪个设计模式?”
这些问题本身没问题,但问题在于提问背后的预设:有一个“正确”的答案,只要我找到了,就能一劳永逸。
📦 环境:输入输出确定
学科思维面对的“问题”,都是被精心设计过的。题目的条件清清楚楚,边界条件明明白白,输入是什么、输出是什么,都在题目里写着。
竞赛更是如此。你在4个小时里面对的几道题,每一道都有明确的输入范围、时间限制、内存限制。你不需要考虑“用户会不会输入非法数据”,因为题目保证不会;你不需要考虑“这个算法三年后还能不能跑”,因为代码写完就扔。
⚡ 反馈:对错立等可见
写一道题,提交,马上知道对错。错了可以马上改,改完再交,直到通过。这种即时反馈,会让人形成一种习惯:先写再说,错了再调。
💻 代码观:跑通即结束
在学科思维的视角里,代码的“生命周期”止于提交通过的那一刻。代码写完,问题解决,任务完成。至于这段代码以后会不会被别人看,会不会被修改,不在考虑范围内。
这种思维模式有问题吗?没有。相反,它在它适用的场景里,非常高效。
问题是,工作的场景,和学科的场景,是完全不同的。
工作的场景是:
- 没有标准答案,只有“当下最合适”的方案
- 输入不确定,输出也不确定(需求会变)
- 反馈很慢(一个bug可能三个月后才被发现)
- 代码的生命周期很长,长到你可能已经忘了自己写过它
于是,带着学科思维进入职场的人,会经历一系列“碰撞”。这些碰撞,就是工程思维开始萌芽的地方。
第二部分:转折——工程思维是从哪里开始“长”出来的?
工程思维不是某一天突然学会的。它是在一次次具体的、甚至有点疼的经历里,一点点长出来的。
我试着还原四个最常见的“转折点”。你可能已经经历过其中一些,也可能正在经历。
第一次转折:被自己的代码坑过之后 😱
这是一个非常经典的场景:
三个月前,你写了一个功能。当时需求急,你加班到半夜,终于跑通了。上线,没问题。然后你转头去做别的项目,把这段代码忘得干干净净。
三个月后,产品经理过来说:这个功能要加一个新需求,改动不大,就加一个判断条件。
你打开自己三个月前写的文件。看到第一行,你愣了一下。
这个文件叫什么来着?temp.js?好吧,不重要。打开文件,你看到:
// 三个月前的你写的代码
function handleData(data) {
let a = data.list.map(i => i.value).filter(v => v > 10).reduce((p, c) => p + c, 0)
let b = data.list.map(i => i.count).filter(v => v > 5).reduce((p, c) => p * c, 1)
return { a, b, c: a + b }
}
没有注释。没有类型。变量名是a、b、i、v。
你盯着这段代码,努力回忆:a是什么?b是什么?为什么最后要返回一个c?c是干嘛用的?
你试着改。加了那个判断条件。跑测试,挂了。你开始调试,发现原来a和b在某些情况下会是undefined,但你原来的代码没处理。你修了undefined,测试又挂了,因为你改的地方影响了另一个模块。
折腾了两个小时,你终于改完了。提交代码的时候,你在心里骂了一句:“谁他妈写的这破代码!”
然后你看到git blame。
作者:你自己。
这一刻,是很多人的第一次“觉醒”。
你第一次意识到:代码不仅是写给机器看的,更是写给未来的自己看的。 而未来的自己,可能会忘记现在自己想的一切。
这个意识,是可维护思维的萌芽。
从那以后,你开始注意一些以前不在意的事:
- 文件名要起得有意义,不能叫temp.js
- 变量名要能表达意图,不能叫a、b
- 复杂的逻辑要加注释,不是为了装模作样,是为了三个月后的自己
- 函数不能太长,太长了自己都看不懂
三个月后,同样的需求,你可能会这样写:
/**
* 处理用户数据,计算活跃度指标
* @param {Object} data – 原始数据对象
* @param {Array} data.list – 用户行为列表
* @returns {Object} 包含计算结果的指标对象
*/
function calculateUserActivityMetrics(data) {
// 防御性编程:如果数据不存在,返回默认值
if (!data?.list?.length) {
return {
totalValue: 0,
productCount: 1,
compositeScore: 1
};
}
// 计算总活跃值:所有value大于10的项之和
const totalActiveValue = data.list
.filter(item => item.value > 10)
.reduce((sum, item) => sum + item.value, 0);
// 计算产品覆盖数:所有count大于5的项之积(用于某种加权算法)
const productCoverage = data.list
.filter(item => item.count > 5)
.reduce((product, item) => product * item.count, 1);
// 复合得分是两者的结合
const compositeScore = totalActiveValue + productCoverage;
return {
totalValue: totalActiveValue,
productCount: productCoverage,
compositeScore
};
}
你不是为了“规范”而做这些,你是被自己坑怕了。
第二次转折:被线上故障教育过之后 🔥
这是另一个常见的场景:
你开发了一个功能,测试环境跑得好好的,各种case都覆盖了。上线,睡觉。
第二天早上醒来,打开手机,十几条@你的消息。
“线上挂了。”
“用户反馈页面打不开。”
“快看看怎么回事。”
你打开电脑,登录服务器,查看日志。
$ tail -f /var/log/app.log
# 空空如也,什么都没有
没有日志。
你写的代码,console.log都没加。上线前你觉得“肯定没问题”,就删掉了调试代码。现在你想知道哪里出了错,但代码像个黑盒,什么都不告诉你。
你只能靠猜。可能是这个接口挂了?可能是那个数据不对?你加了日志,重新上线,等用户触发bug。十分钟后,日志出来了。哦,原来是第三方服务返回的数据格式变了。
// 你原来的代码
function renderUserInfo(userId) {
fetch(`/api/user/${userId}`)
.then(res => res.json())
.then(data => {
// 假设data.data.profile一定存在
document.getElementById('user-name').textContent = data.data.profile.name;
document.getElementById('user-avatar').src = data.data.profile.avatar;
})
}
第三方服务返回的变成了:
{
"data": null,
"message": "user not found"
}
于是data.data.profile直接报错,页面白屏。
你修了,上线,松了一口气。
但你知道,这只是运气好。如果用户不触发,你可能到现在还找不到原因。
这一刻,你意识到另一件事:代码上线不是终点,是可观测的起点。
功能“正确”只是最低要求。更重要的是,当它出问题的时候,你能不能快速知道“哪里出了问题”。
这就是可观测意识的萌芽。
从那以后,你开始这样写代码:
// 增加可观测性的版本
function renderUserInfo(userId) {
// 1. 关键路径打日志
console.log(`[renderUserInfo] started for userId: ${userId}`);
fetch(`/api/user/${userId}`)
.then(res => res.json())
.then(data => {
// 2. 记录接口返回数据(调试用)
console.log(`[renderUserInfo] API response:`, data);
// 3. 防御性判断 + 错误边界
if (!data?.data?.profile) {
console.warn(`[renderUserInfo] profile not found for user ${userId}`, data);
document.getElementById('user-info').innerHTML = '<div class="error">用户信息加载失败</div>';
return;
}
// 4. 正常渲染
document.getElementById('user-name').textContent = data.data.profile.name;
document.getElementById('user-avatar').src = data.data.profile.avatar;
// 5. 成功日志
console.log(`[renderUserInfo] success for userId: ${userId}`);
})
.catch(err => {
// 6. 异常捕获 + 错误上报
console.error(`[renderUserInfo] failed for userId: ${userId}`, err);
// 可以上报到监控系统
reportError('renderUserInfo', { userId, error: err.message });
document.getElementById('user-info').innerHTML = '<div class="error">网络异常,请稍后重试</div>';
});
}
你开始分层打日志。用户行为日志、接口请求日志、异常捕获日志。你开始加错误边界,让页面不至于因为一个组件崩溃就整个白屏。你开始埋点,不是为了数据分析,是为了出问题时能回溯用户的操作路径。
你做这些,不是产品经理要求的,不是领导要求的,是被线上故障教育出来的。
第三次转折:和人协作摩擦过之后 🤝
再换一个场景:
你加入一个新团队,接手一个老项目。打开代码,你发现一个文件里有好几个函数,命名是handle1、handle2、handle3。
// 你看到的代码
function handle1(e) {
const val = e.target.value;
if (val.length > 10) {
// do something
}
}
function handle2() {
// 50行代码
}
function handle3() {
// 又是50行
}
你问旁边的同事:“这个handle1是干嘛的?”
同事看了一眼,说:“哦,那个是处理用户点击头像的逻辑。”
你心想:那为什么不叫handleUserAvatarClick?
你继续看。发现另一个文件里有3000行代码,一个组件从头写到尾,没有拆分子组件。
// 一个3000行的组件
function UserDashboard() {
// 0-500行:用户信息相关逻辑
const [user, setUser] = useState(null);
// … 省略200行
// 500-1000行:订单列表相关逻辑
const [orders, setOrders] = useState([]);
// … 省略200行
// 1000-1500行:消息通知相关逻辑
const [messages, setMessages] = useState([]);
// … 省略200行
// 1500-3000行:渲染
return (
<div>
{/* 用户信息区域,500行JSX */}
{/* 订单列表区域,500行JSX */}
{/* 消息通知区域,500行JSX */}
</div>
);
}
你想改一个按钮的样式,找了半天,发现样式写在另一个文件里,用了一个叫.a1b2c3的类名。
你问同事:“这个类名是什么意思?”
同事说:“那个是CSS Modules自动生成的,不用管。”
你心想:但我想找到它在哪定义的啊。
你开始改需求。改完,提测。测试同学说:“这个按钮的样式有问题,和设计稿不一样。”
你检查,发现你改的地方影响了另一个页面的样式,因为你的CSS类名冲突了。
同事过来说:“你改的时候没看吗?那个类名其他地方也在用。”
你心想:我怎么知道?
这是协作中的常见摩擦。而摩擦的核心原因往往是:代码不是写给自己看的,是写给团队看的。
一个人写代码,怎么命名都可以,怎么组织都可以,只要自己能看懂就行。但一群人写代码,就必须有一致的规则,否则就会互相踩脚、互相看不懂、互相改坏对方的代码。
经过几次这样的摩擦,你开始理解为什么要有代码规范。于是团队一起定了规范,代码变成了这样:
// 函数命名清晰
function handleUserAvatarClick(event) {
const userId = event.currentTarget.dataset.userId;
navigateToUserProfile(userId);
}
function handleUserProfileNavigation(userId) {
// 具体跳转逻辑
}
// 组件拆分
// UserInfo.jsx
function UserInfo({ user }) {
return <div>{user.name}</div>;
}
// OrderList.jsx
function OrderList({ orders }) {
return orders.map(order => <OrderItem key={order.id} order={order} />);
}
// MessageList.jsx
function MessageList({ messages }) {
return messages.map(msg => <MessageItem key={msg.id} message={msg} />);
}
// UserDashboard.jsx
function UserDashboard() {
const [user] = useUser();
const [orders] = useOrders();
const [messages] = useMessages();
return (
<div>
<UserInfo user={user} />
<OrderList orders={orders} />
<MessageList messages={messages} />
</div>
);
}
// 使用BEM命名规范
.user-dashboard {
&__header { … }
&__content { … }
&__footer { … }
}
.user-info {
&__avatar { … }
&__name { … }
}
这个过程会让人意识到:
- 代码规范不是束缚,是降低沟通成本的工具
- 组件拆分不是炫技,是让代码能被多人并行修改的前提
- 注释不是废话,是告诉别人“这里为什么这么写”的说明书
- Code Review不是找茬,是让代码质量不被个人水平拉平的手段
这种意识,可以叫协作思维。它不是从书上学来的,是被协作摩擦磨出来的。
第四次转折:看着自己的项目“长大”之后 🌱
最后一个场景,可能需要更长的时间跨度:
你参与了一个项目,从零开始写起。一开始,项目很简单,几个文件,几千行代码。
// 项目初期:简洁明了
// utils.js
export function formatDate(date) { … }
export function validateEmail(email) { … }
// api.js
export function fetchUser(id) { … }
export function fetchOrders(userId) { … }
// components/UserProfile.jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
半年后,项目越来越大。十几个模块,几万行代码。
// 半年后:开始出现“胖文件”
// utils.js 已经2000行
export function formatDate(date) { … }
export function formatTime(date) { … }
export function formatDateTime(date) { … }
export function validateEmail(email) { … }
export function validatePhone(phone) { … }
export function validateIdCard(idCard) { … }
// … 还有几十个工具函数
一年后,项目已经几十万行代码。原来的文件,有的已经3000行。
// 一年后:一个文件里什么都有
// userHelper.js 3000行
// 这里有用户相关的工具函数
// 也有用户相关的业务逻辑
// 还有用户相关的UI组件
// 甚至还有用户相关的样式常量
你想重构,发现牵一发而动全身。你改一个工具函数,十几个地方报错。你加一个功能,不知道应该放在哪个模块里。
两年后,你离开这个项目,去忙别的。又过了一年,你回来看看,发现项目已经面目全非。你当年写的代码还在,但周围多了很多你不认识的代码。
这个过程,会让人产生一种特殊的感受:代码是会“长大”的。
刚写的时候,它是个婴儿,你可以抱着它、哄着它。但随着时间的推移,它会自己长出新的枝叶,变得越来越复杂,越来越难以控制。如果你一开始没有给它留出“生长”的空间,它就会乱长,长成一团乱麻。
这就是演进意识的萌芽。
经历过这些之后,再写新项目时,你会从一开始就考虑“生长”的问题:
// 项目结构设计(考虑未来演进)
src/
├── modules/ # 按业务模块划分,不是按技术类型
│ ├── user/ # 用户模块
│ │ ├── components/ # 模块内私有组件
│ │ ├── hooks/ # 模块内自定义hooks
│ │ ├── utils/ # 模块内工具函数
│ │ ├── api/ # 模块内接口定义
│ │ └── index.js # 模块出口
│ ├── order/ # 订单模块
│ └── product/ # 产品模块
├── shared/ # 真正跨模块共享的代码
│ ├── components/ # 通用组件
│ ├── hooks/ # 通用hooks
│ └── utils/ # 通用工具函数
└── app/ # 应用入口、路由、全局配置
// 预留扩展点的设计
// 不是写死配置,而是设计成可扩展的
const tableConfig = {
columns: [
{ key: 'name', title: '姓名', render: (val) => `<b>${val}</b>` },
{ key: 'age', title: '年龄' }
],
// 预留自定义列的能力
extendColumns: (userColumns) => {
return […userColumns, …tableConfig.columns];
},
// 预留数据预处理的能力
processData: (data) => {
// 默认处理逻辑
return data.map(item => ({ …item, key: item.id }));
}
};
从那以后,你在写代码的时候,会开始思考:
- 这段代码三个月后还能看懂吗?
- 这个设计能支撑到明年吗?
- 如果这个功能要扩展,需要在哪些地方留出口?
- 如果这个人离职了,他的代码能被人接住吗?
你不再只想着“今天怎么写完这个功能”,你开始想“这个功能在未来会变成什么样”。
这四个转折点,是工程思维形成的常见路径。
| 😱 被自己的代码坑过 | 可维护意识 | 代码是写给未来的自己看的 |
| 🔥 被线上故障教育过 | 可观测意识 | 上线不是终点,是可观测的起点 |
| 🤝 和人协作摩擦过 | 协作意识 | 代码是写给团队看的 |
| 🌱 看着项目长大过 | 演进意识 | 代码是会长大的 |
每一次转折,都是在具体的、真实的场景里,被现实推着往前走一步。
第三部分:沉淀——工程思维的四个层次
经历了上面的转折,思维会慢慢沉淀成一些相对稳定的意识。我把它们归纳为四个层次。
这四个层次不是学来的,是“长”出来的——每经历一次教训,就多一层意识。
层次一:可维护意识 🧹
这是最基础、也是最先形成的一层。
可维护意识体现在:
命名:变量、函数、组件、文件的命名,要能表达意图
// ❌ 不好的命名
const d = new Date();
const tmp = getUserData();
function hdl() { … }
// ✅ 好的命名
const currentDate = new Date();
const userData = getUserData();
function handleUserLogout() { … }
注释:复杂的逻辑要加注释,重点不是“是什么”,而是“为什么”
// ❌ 解释“是什么”(废话)
// 循环遍历数组
for (let i = 0; i < list.length; i++) { … }
// ✅ 解释“为什么”(有价值)
// 这里不用Array.prototype.map是因为需要支持IE11
for (let i = 0; i < list.length; i++) { … }
粒度:函数不能太长,组件不能太大,拆到合适的粒度
// ❌ 一个函数做太多事
function processUserData(user) {
// 验证
if (!user.email) throw new Error('email required');
if (!user.name) throw new Error('name required');
// 格式化
const formattedUser = {
…user,
email: user.email.toLowerCase(),
name: user.name.trim()
};
// 保存到数据库
db.save(formattedUser);
// 发送欢迎邮件
email.send(formattedUser.email, 'welcome');
// 记录日志
log('user created', formattedUser);
}
// ✅ 拆分成多个小函数
function validateUser(user) { … }
function formatUser(user) { … }
function saveUser(user) { … }
function sendWelcomeEmail(email) { … }
function logUserCreation(user) { … }
function processUserData(user) {
validateUser(user);
const formattedUser = formatUser(user);
saveUser(formattedUser);
sendWelcomeEmail(formattedUser.email);
logUserCreation(formattedUser);
}
可维护意识背后的思维是:时间维度的考量。
写代码的时候,不仅要考虑“现在能不能跑”,还要考虑“三个月后还能不能看懂”。这是一种把时间变量引入思考的能力。
层次二:可观测意识 📊
这是在被线上故障教育之后,慢慢形成的。
可观测意识体现在:
日志:关键路径要打日志,异常要打日志,日志要有上下文
// ❌ 没有日志
function checkout(cartId) {
const cart = getCart(cartId);
const order = createOrder(cart);
processPayment(order);
}
// ✅ 有日志
function checkout(cartId) {
console.log(`[checkout] started`, { cartId, timestamp: Date.now() });
try {
const cart = getCart(cartId);
console.log(`[checkout] cart loaded`, { cartId, itemCount: cart.items.length });
const order = createOrder(cart);
console.log(`[checkout] order created`, { orderId: order.id, total: order.total });
const payment = processPayment(order);
console.log(`[checkout] payment processed`, { paymentId: payment.id, status: payment.status });
console.log(`[checkout] completed`, { cartId, orderId: order.id });
return order;
} catch (error) {
console.error(`[checkout] failed`, { cartId, error: error.message, stack: error.stack });
throw error;
}
}
错误边界:组件要容错,页面要兜底
// ❌ 无错误边界
function UserProfile({ userId }) {
const user = useUser(userId);
return <div>{user.profile.name}</div>; // 如果user或profile为null,直接崩溃
}
// ✅ 有错误边界
function UserProfile({ userId }) {
const user = useUser(userId);
if (!user) {
return <Loading />;
}
if (!user.profile) {
return <ErrorMessage message="用户资料不完整" />;
}
try {
return <div>{user.profile.name}</div>;
} catch (error) {
return <ErrorMessage message="渲染用户资料时出错" />;
}
}
// 或者使用Error Boundary组件
class UserProfileErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
reportError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>用户资料加载失败,请稍后重试</div>;
}
return this.props.children;
}
}
// 使用
<UserProfileErrorBoundary>
<UserProfile userId={123} />
</UserProfileErrorBoundary>
可观测意识背后的思维是:不确定性下的安全感。
你无法保证代码永远不出错,但你可以保证出错的时候能快速定位。这是一种对“意外”的敬畏,也是一种对“安全感”的追求。
层次三:防御意识 🛡️
这是在和各种“意外”打过交道之后形成的。
防御意识体现在:
空值判断:永远假设接口返回的数据可能为空
// ❌ 乐观代码
function displayUserName(user) {
return user.profile.name; // 如果user为null,直接报错
}
// ✅ 防御性代码
function displayUserName(user) {
// 多层防御
if (!user) {
return '未知用户';
}
if (!user.profile) {
return '用户资料不完整';
}
return user.profile.name || '匿名用户';
}
// 或者使用可选链和空值合并
function displayUserName(user) {
return user?.profile?.name ?? '匿名用户';
}
降级方案:第三方服务挂了怎么办?
// ❌ 无降级方案
async function loadUserAvatar(userId) {
// 假设用了某个第三方图片服务
const response = await fetch(`https://cdn.thirdparty.com/avatars/${userId}.jpg`);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
// ✅ 有降级方案
async function loadUserAvatar(userId) {
try {
// 尝试从第三方CDN加载
const response = await fetch(`https://cdn.thirdparty.com/avatars/${userId}.jpg`);
if (response.ok) {
const blob = await response.blob();
return URL.createObjectURL(blob);
}
throw new Error('CDN加载失败');
} catch (error) {
console.warn('CDN加载失败,使用本地默认头像', error);
// 降级到本地默认头像
return '/images/default-avatar.png';
}
}
容错处理:用户网络断了怎么办?
// ❌ 无容错
async function submitForm(data) {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data)
});
return response.json();
}
// ✅ 有容错(请求重试)
async function submitForm(data, retryCount = 3) {
for (let i = 0; i < retryCount; i++) {
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data),
// 设置超时
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.warn(`提交失败,第${i + 1}次重试`, error);
// 最后一次重试失败,才真正抛出错误
if (i === retryCount – 1) {
throw new Error('提交失败,请检查网络后重试');
}
// 等待一段时间后重试(指数退避)
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}
防御意识背后的思维是:对“意外”的接纳。
你不再天真地假设“一切都会按预期运行”。你知道网络会断,接口会挂,用户会瞎操作,第三方会改协议。你不再抱怨这些“意外”,而是在代码里给它们留位置。
层次四:演进意识 🌳
这是最难形成的一层,往往需要经历过“看着项目长大”的过程。
演进意识体现在:
扩展点:在可能变化的地方留出口
// ❌ 写死,无法扩展
function calculatePrice(product) {
// 只有一个打折规则
if (product.category === '电子') {
return product.price * 0.9;
}
return product.price;
}
// ✅ 可扩展的设计
const discountRules = [
{ category: '电子', discount: 0.9 },
{ category: '图书', discount: 0.85 },
{ category: '服装', discount: 0.8 }
];
function calculatePrice(product, rules = discountRules) {
const rule = rules.find(r => r.category === product.category);
if (rule) {
return product.price * rule.discount;
}
return product.price;
}
// 甚至可以支持动态添加规则
function createPriceCalculator(initialRules = []) {
let rules = […initialRules];
return {
calculate(product) {
const rule = rules.find(r => r.category === product.category);
return rule ? product.price * rule.discount : product.price;
},
addRule(category, discount) {
rules.push({ category, discount });
},
removeRule(category) {
rules = rules.filter(r => r.category !== category);
}
};
}
const calculator = createPriceCalculator(discountRules);
// 双十一临时加规则
calculator.addRule('家居', 0.75);
抽象程度:抽象到什么程度,既不过度设计,又不让未来改不动
// ❌ 过度设计(写每一行都在想“以后可能会复用”)
class AbstractUserRepository {
constructor(database) {
this.database = database;
}
async findById(id, options = {}) {
const { includeDeleted = false, select = ['*'], …rest } = options;
// 几十行通用查询逻辑
}
// … 还有几十个抽象方法
}
// ❌ 完全没有抽象(所有代码写死在组件里)
function UserPage() {
const [users, setUsers] = useState([]);
useEffect(() => {
// 直接在组件里写fetch
fetch('/api/users')
.then(res => res.json())
.then(setUsers);
}, []);
// 直接在组件里写过滤逻辑
const filteredUsers = users.filter(u => u.active);
// 直接在组件里写排序逻辑
const sortedUsers = […filteredUsers].sort((a, b) => a.name.localeCompare(b.name));
// … 全部混在一起
}
// ✅ 合适的抽象(在需要的地方抽象)
// hooks/useUsers.js – 抽取数据逻辑
function useUsers() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
return { users, loading, error };
}
// utils/userFilters.js – 抽取过滤逻辑
function filterActiveUsers(users) {
return users.filter(u => u.active);
}
function sortUsersByName(users) {
return […users].sort((a, b) => a.name.localeCompare(b.name));
}
// components/UserPage.jsx – 只负责组合
function UserPage() {
const { users, loading, error } = useUsers();
if (loading) return <Loading />;
if (error) return <ErrorMessage error={error} />;
const activeUsers = filterActiveUsers(users);
const sortedUsers = sortUsersByName(activeUsers);
return <UserList users={sortedUsers} />;
}
演进意识背后的思维是:对“变化”的接纳。
你不再试图“一步到位”写出完美的代码。你知道代码是会长的,需求是会变的,人是会换的。你的任务不是写出“永远不改”的代码,而是写出“能适应变化”的代码。
这四个层次,不是线性进阶的关系。它们往往交织在一起,在不同的场景里被调用。
但有一点是确定的:它们都不是看书看来的,都是在具体的实践里,一点点“长”出来的。
第四部分:加速——如何让这个过程发生得更快? ⏩
前面说了,工程思维的形成需要时间,需要经历,需要踩坑。但能不能让这个过程快一点?能不能不用踩那么多坑,也能长出这些意识?
可以。有意识地做一些事,能加速这个过程。
1. 多读“旧代码” 📖
不是读别人的开源项目,是读自己三个月前写的代码,读别人维护了三年的项目。
# 看看自己三个月前的代码
git log –since="3 months ago" –author="你的名字" –pretty=format:"%h %s" | head -20
# 挑一个文件看看
git show 某个commit的hash:path/to/file.js
读自己三个月前的代码,是一种很好的“自我教育”。如果你读的时候想骂人,说明你这三个月进步了。如果你读的时候觉得“写得挺好”,那可能你这三个月没什么进步。
读别人维护了三年的项目,是一种“历史感”的训练。你能看到代码是怎么一点点长大的,哪些设计扛住了时间的考验,哪些设计在半年后就撑不住了。
这种阅读,会让你对“时间”有更具体的感知。
2. 多复盘“故障” 🔍
不是只看自己的故障,也看别人的事故复盘。
很多公司内部都有“故障复盘文档”,外面也有各种技术博客分享的线上事故分析。这些东西是很好的教材。你可以问自己:
- 这个故障是怎么发生的?
- 如果是我,能避免吗?
- 如果是我,能快速定位吗?
- 我的代码里有类似的隐患吗?
别人的坑,也是可以借用的经验。
3. 多参与“交接” 🔄
接手别人的项目,是一种训练;把自己的项目交给别人,是另一种训练。
接手别人的项目,你会切身体会到“什么样的代码好接”、“什么样的文档有用”。这些体会,会成为你以后写代码时的参照。
把自己的项目交给别人,你会感受到“什么样的代码好交”。如果你交接的时候,发现自己要讲很久别人才懂,说明你的代码还有优化的空间。
# 交接文档模板
## 项目概述
– 项目是做什么的?
– 主要技术栈是什么?
## 目录结构
– 核心模块在哪里?
– 配置文件在哪里?
## 开发环境搭建
– 需要哪些依赖?
– 如何启动?
## 常见问题
– 哪些地方容易出bug?
– 出了bug怎么排查?
## 部署流程
– 如何构建?
– 如何上线?
– 如何回滚?
## 遗留问题
– 有哪些技术债务?
– 哪些地方需要重构?
4. 多问“时间维度的题” ⏰
在日常写代码的时候,多问自己几个问题:
- 这段代码三个月后还能看懂吗?
- 这个设计能支撑到明年吗?
- 如果这个人离职了,代码能被人接住吗?
- 如果这个需求翻倍了,架构扛得住吗?
这些问题,能帮你把“时间”这个变量,带入当下的思考。
// 写代码时的自问清单
function writeCode() {
// 功能实现(基础)
implement();
// 问自己:这段代码三个月后我还看得懂吗?
if (!readable()) {
addComments();
renameVariables();
extractFunctions();
}
// 问自己:如果出错了,我能知道吗?
if (!observable()) {
addLogging();
addErrorBoundaries();
}
// 问自己:如果依赖挂了,能扛住吗?
if (!defensive()) {
addNullChecks();
addFallbacks();
addRetries();
}
// 问自己:如果需求变了,好改吗?
if (!evolvable()) {
addExtensionPoints();
adjustAbstraction();
}
}
第五部分:结语——工程思维的本质是什么
写到这里,可以试着回答一开始的问题:工程思维到底是什么?
我的理解是:工程思维,是时间感和系统感的建立过程。
-
时间感:能看见代码的过去、现在和未来。知道代码是怎么长成这样的,知道它在未来会变成什么样,知道今天写的每一行代码,都会影响三个月后的自己。
-
系统感:能看见代码之外的东西。看见写代码的人,看见改代码的人,看见用代码的人,看见代码依赖的环境、服务、网络。知道自己写的代码,不是一个孤立的个体,而是一个更大系统的一部分。
这两个“感”,都不是天生的,都是在实践中慢慢长出来的。
回到标题:代码之外的成长。
真正的成长,往往不在代码本身。
在代码之外,有被自己坑过之后长出的谨慎;
在代码之外,有被线上故障教育之后长出的敬畏;
在代码之外,有和协作摩擦之后长出的边界感;
在代码之外,有时间沉淀之后长出的判断力。
这些,才是“资深工程师”和“普通开发者”之间,真正的差距。
一张图总结工程思维的形成过程
#mermaid-svg-FyLAMhAqotNVCwoS{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FyLAMhAqotNVCwoS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FyLAMhAqotNVCwoS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FyLAMhAqotNVCwoS .error-icon{fill:#552222;}#mermaid-svg-FyLAMhAqotNVCwoS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FyLAMhAqotNVCwoS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FyLAMhAqotNVCwoS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FyLAMhAqotNVCwoS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FyLAMhAqotNVCwoS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FyLAMhAqotNVCwoS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FyLAMhAqotNVCwoS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FyLAMhAqotNVCwoS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FyLAMhAqotNVCwoS .marker.cross{stroke:#333333;}#mermaid-svg-FyLAMhAqotNVCwoS svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FyLAMhAqotNVCwoS p{margin:0;}#mermaid-svg-FyLAMhAqotNVCwoS .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-FyLAMhAqotNVCwoS .cluster-label text{fill:#333;}#mermaid-svg-FyLAMhAqotNVCwoS .cluster-label span{color:#333;}#mermaid-svg-FyLAMhAqotNVCwoS .cluster-label span p{background-color:transparent;}#mermaid-svg-FyLAMhAqotNVCwoS .label text,#mermaid-svg-FyLAMhAqotNVCwoS span{fill:#333;color:#333;}#mermaid-svg-FyLAMhAqotNVCwoS .node rect,#mermaid-svg-FyLAMhAqotNVCwoS .node circle,#mermaid-svg-FyLAMhAqotNVCwoS .node ellipse,#mermaid-svg-FyLAMhAqotNVCwoS .node polygon,#mermaid-svg-FyLAMhAqotNVCwoS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FyLAMhAqotNVCwoS .rough-node .label text,#mermaid-svg-FyLAMhAqotNVCwoS .node .label text,#mermaid-svg-FyLAMhAqotNVCwoS .image-shape .label,#mermaid-svg-FyLAMhAqotNVCwoS .icon-shape .label{text-anchor:middle;}#mermaid-svg-FyLAMhAqotNVCwoS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FyLAMhAqotNVCwoS .rough-node .label,#mermaid-svg-FyLAMhAqotNVCwoS .node .label,#mermaid-svg-FyLAMhAqotNVCwoS .image-shape .label,#mermaid-svg-FyLAMhAqotNVCwoS .icon-shape .label{text-align:center;}#mermaid-svg-FyLAMhAqotNVCwoS .node.clickable{cursor:pointer;}#mermaid-svg-FyLAMhAqotNVCwoS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FyLAMhAqotNVCwoS .arrowheadPath{fill:#333333;}#mermaid-svg-FyLAMhAqotNVCwoS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FyLAMhAqotNVCwoS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FyLAMhAqotNVCwoS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FyLAMhAqotNVCwoS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FyLAMhAqotNVCwoS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FyLAMhAqotNVCwoS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FyLAMhAqotNVCwoS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FyLAMhAqotNVCwoS .cluster text{fill:#333;}#mermaid-svg-FyLAMhAqotNVCwoS .cluster span{color:#333;}#mermaid-svg-FyLAMhAqotNVCwoS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FyLAMhAqotNVCwoS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FyLAMhAqotNVCwoS rect.text{fill:none;stroke-width:0;}#mermaid-svg-FyLAMhAqotNVCwoS .icon-shape,#mermaid-svg-FyLAMhAqotNVCwoS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FyLAMhAqotNVCwoS .icon-shape p,#mermaid-svg-FyLAMhAqotNVCwoS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FyLAMhAqotNVCwoS .icon-shape rect,#mermaid-svg-FyLAMhAqotNVCwoS .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FyLAMhAqotNVCwoS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FyLAMhAqotNVCwoS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FyLAMhAqotNVCwoS :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
学科思维起点
被自己代码坑过
被线上故障教育
与人协作摩擦
看着项目长大
可维护意识
可观测意识
协作意识
演进意识
工程思维
时间感
系统感
最后,留两个问题给你:
你的工程思维,是在哪个节点开始“长”出来的?
如果你带新人,会希望他们少踩哪些坑?
欢迎在评论区分享。你的经历,可能是另一个人的“加速器”。

