AI 时代的工程实践指南:别让 AI 把你的代码库变成屎山
写在前面:你可能正在经历的痛苦
如果你正在用 Claude、Cursor、GitHub Copilot 这些 AI 工具写代码,下面这些场景你一定不陌生:
场景 1:需求对不上
你:“帮我做个用户管理功能”AI:(噼里啪啦写了 500 行代码)你:(一看)“我要的是批量导入啊,你这只能一个个添加!”AI:“好的,我来改”(又写了 300 行)你:“不对不对,我要的是……”
一个下午过去了,代码越改越乱。
场景 2:代码能跑但看不懂
AI 生成的代码能跑,测试也过了。但是…… 10 个文件互相调用,改一个地方要动 5 个文件。新来的同事看了一眼:“这什么玩意儿?”你:“我也不知道,AI 写的……”
场景 3:Bug 越修越多
修了一个 Bug,冒出来三个新 Bug。AI 说:“我帮你修”,然后又引入了两个 Bug。你开始怀疑人生:用 AI 到底是加速了还是减速了?
场景 4:三个月后的噩梦
项目刚开始:AI 真香,代码飞快!三个月后:改不动了,每次改动都心惊胆战。六个月后:重构吧,这坨代码没救了。
如果你中了 2 个以上,这篇文章就是为你写的。
核心问题:AI 让你写得快,但不会让代码变好
Matt Pocock(TypeScript 圈的知名专家)在用了一年多 AI 编程工具后,总结出了一个残酷的真相:
AI 能让你以 10 倍速度制造混乱,也能以 10 倍速度创造价值。关键在于:你有没有一套适配 AI 的工程实践。
他开源了自己每天在用的 Skills 工具集,专门解决 AI 编程中的四大陷阱。这不是又一个“AI 框架”,而是一套让 AI 成为你工程伙伴的方法论。
四大陷阱与破解之道
陷阱 1:需求错位 —— “我以为你懂我”
真实场景
产品经理:“做个订单取消功能。”
你让 AI 写了代码,产品经理一看:
“不对啊,我要的是:
已支付的订单取消要退款
已发货的不能取消
部分商品可以单独取消
取消后库存要回滚
要给用户发通知“
你:“你早说啊!”产品经理:“我以为你知道啊!”
这就是需求错位。 《程序员修炼之道》里说得好:“没有人确切知道自己想要什么。” 人和人之间都会错位,何况人和 AI?
破解之道:让 AI 先“拷问”你
/grill-with-docs 技能就是干这个的。在写代码前,让 AI 对你的计划进行“灵魂拷问”:
你:做个订单取消功能
AI:好的,我先问几个问题:
1. 哪些状态的订单可以取消?
- 待支付?已支付?已发货?已完成?
2. 取消后钱怎么办?
- 原路退回?退到余额?多久到账?
3. 库存怎么处理?
- 立即回滚?延迟回滚?
4. 部分取消支持吗?
- 一个订单 3 件商品,能只取消 1 件吗?
5. 取消失败怎么办?
- 退款失败?库存回滚失败?
6. 用户怎么知道?
- 站内信?短信?邮件?
问完这些,你会发现: 你以为的“简单功能”,其实有 20 个分支要处理。
更重要的是,AI 会把这些决策记录到 CONTEXT.md 文档里,下次再做相关功能,它就知道你们项目的“行话”了。
陷阱 2:语言不通 —— “你说的订单和我说的订单不是一个东西”
真实场景
你的项目里有这些概念:
-
Order(订单)
-
Cart(购物车)
-
Payment(支付)
-
Transaction(交易)
AI 不知道你们的定义,结果:
-
有时候把 Order 和 Transaction 混着用
-
有时候 Payment 里又包含了 Order 的逻辑
-
代码里同一个东西有 5 种叫法
三个月后,新来的同事:
“这个 OrderTransaction 和 TransactionOrder 有啥区别?”你:“呃…… 我也不太清楚了……”
破解之道:建立“黑话词典”
CONTEXT.md 就是你项目的“黑话词典”。
没有 CONTEXT.md 的对话:
你:实现订单取消功能,取消后要处理支付和库存
AI:好的(写了 200 行代码,创建了 OrderCancellation、
PaymentRefund、InventoryRollback、
CancellationTransaction 四个类)
有 CONTEXT.md 的对话:
CONTEXT.md 里写着:
- Order:订单,包含 items(商品列表)和 payment(支付信息)
- Order.cancel():取消订单,自动触发退款和库存回滚
- Refund:退款记录,由 Payment.refund() 创建
你:实现订单取消功能
AI:好的,我在 Order 类里加个 cancel() 方法,
它会调用 this.payment.refund() 和
this.items.forEach(item => item.restoreStock())
对比一下:
-
没词典:4 个新类,200 行代码,概念混乱
-
有词典:1 个方法,20 行代码,清晰明了
陷阱 3:盲飞 —— “代码能跑就行了吧?”
真实场景
你:帮我写个用户注册功能
AI:(写了代码)
你:看起来不错,提交!
// 上线后...
用户 A:邮箱里有加号注册不了
用户 B:密码太短也能注册
用户 C:注册成功了但是登录不了
你:AI,帮我修这些 Bug
AI:(改了代码)
// 结果...
用户 D:现在邮箱里有点号也注册不了了
用户 E:密码限制太严格了,我的密码不让用
问题在哪? AI 在“盲飞”—— 它不知道代码跑起来是什么样的。
《程序员修炼之道》说:“反馈速率就是你的速度限制。” 没有反馈,AI 就是在瞎猜。
破解之道:先写测试,再写代码
/tdd 技能教 AI 用“测试驱动开发”:
// 第 1 步:写一个会失败的测试
test('用户可以用有效邮箱注册', async () => {
const user = awaitregister({
email: 'test@example.com',
password: 'SecurePass123!'
});
expect(user.id).toBeDefined();
expect(user.email).toBe('test@example.com');
});
// 运行:❌ 失败(因为还没写代码)
// 第 2 步:写最少的代码让测试通过
asyncfunctionregister({ email, password }) {
const user = await db.users.create({ email, password });
return user;
}
// 运行:✅ 通过
// 第 3 步:再写一个测试
test('邮箱格式不对应该报错', async () => {
awaitexpect(
register({ email: 'not-an-email', password: 'SecurePass123!' })
).rejects.toThrow('邮箱格式不正确');
});
// 运行:❌ 失败
// 第 4 步:加上邮箱验证
asyncfunctionregister({ email, password }) {
if (!isValidEmail(email)) {
thrownewError('邮箱格式不正确');
}
const user = await db.users.create({ email, password });
return user;
}
// 运行:✅ 通过
这样做的好处:
-
AI 有反馈 —— 每写一点代码就知道对不对
-
你有信心 —— 改代码时测试会告诉你有没有搞坏
-
新人友好 —— 测试就是活文档,看测试就知道功能怎么用
关键反模式:别一口气写完所有测试
❌ 错误做法:
写 10 个测试 → 写实现 → 发现测试写错了 → 全部重写
✅ 正确做法:
写 1 个测试 → 写实现 → 通过 → 写下一个测试 → ...
就像爬楼梯,一步一步来,而不是先画 10 层楼梯图纸再一口气爬。
陷阱 4:代码腐化 —— “三个月后看不懂自己写的代码”
真实场景
项目刚开始:
// 清晰简单
functioncheckout(cart) {
const order = createOrder(cart);
processPayment(order);
return order;
}
三个月后,AI 帮你“优化”了 20 次:
// 变成了这样
functioncheckout(cart) {
const validator = newCartValidator(cart);
if (!validator.validate()) {
thrownewValidationError(validator.getErrors());
}
const orderBuilder = newOrderBuilder();
orderBuilder.setCart(cart);
orderBuilder.setUser(cart.user);
orderBuilder.setShippingAddress(cart.shippingAddress);
const order = orderBuilder.build();
const paymentProcessor = PaymentProcessorFactory.create(order.paymentMethod);
const paymentResult = await paymentProcessor.process(order);
if (paymentResult.status === 'success') {
order.setPaymentId(paymentResult.id);
await order.save();
awaitnewOrderNotifier(order).notify();
}
return order;
}
问题:
-
原来 3 行代码能做的事,现在 20 行
-
引入了 5 个新类,每个类都很“浅”(接口复杂,功能简单)
-
改一个地方要动 5 个文件
《软件设计哲学》里说:“最好的模块是深的 —— 简单的接口,强大的功能。”
破解之道:定期“体检”代码
/improve-codebase-architecture 技能帮你找出这些问题:
AI 分析后:
发现 3 个"浅层模块":
1. CartValidator
- 问题:只是简单调用 cart.items.forEach(validate)
- 建议:删掉这个类,把验证逻辑放到 Cart 类里
- 好处:少一个类,逻辑更集中
2. OrderBuilder
- 问题:只是把参数传来传去
- 建议:Order 构造函数直接接收 cart
- 好处:代码从 5 行变成 1 行
3. PaymentProcessorFactory
- 问题:只有一个 create 方法,而且只支持一种支付方式
- 建议:等有第二种支付方式再抽象
- 好处:不要过度设计
“深层模块”vs“浅层模块”的比喻:
浅层模块 = 万能遥控器
-
表面上 100 个按钮,看起来很强大
-
实际上就是把 5 个遥控器的按钮都搬过来
-
你还是要记住每个按钮是干啥的
深层模块 = Siri
-
接口简单:说句话就行
-
功能强大:背后调用了一堆 API
-
你不用管它怎么实现的
好代码应该像 Siri,不是万能遥控器。
四个核心技能深度解析
技能 1:/grill-with-docs —— 开工前的“灵魂拷问”
为什么需要它?
你有没有遇到过这种情况:
你:做个用户搜索功能
AI:好的(写了 200 行代码)
你:不对,我要的是模糊搜索
AI:好的(改了 100 行)
你:还要支持按标签筛选
AI:好的(又改了 150 行)
你:还要支持排序
AI:...(开始重构)
问题在哪? 你以为自己想清楚了,其实没有。AI 也不知道该问什么。
它是怎么工作的?
第 1 步:AI 开始“拷问”
你:做个用户搜索功能
AI:好的,我先确认几个问题:
1. 搜索范围
- 只搜用户名?还是也搜邮箱、手机号?
- 要搜用户的其他信息吗(比如公司、职位)?
2. 搜索方式
- 精确匹配还是模糊匹配?
- 支持拼音搜索吗?
- 支持缩写吗(比如"张三"搜"zs")?
3. 结果展示
- 一次显示多少条?
- 需要分页吗?
- 需要高亮匹配的部分吗?
4. 性能要求
- 用户量级是多少?
- 搜索速度要求多快?
- 需要缓存吗?
5. 权限控制
- 所有人都能搜所有用户吗?
- 需要隐藏某些用户吗?
第 2 步:你回答问题的过程中,发现了很多之前没想到的
你:
1. 搜用户名和邮箱就行
2. 模糊匹配,拼音和缩写暂时不需要
3. 一次 20 条,要分页,要高亮
4. 现在 1000 个用户,预计一年内到 1 万
5. 对,有些内部用户不能被搜到
AI:明白了。我还发现一个问题:
你们现有的 User 模型里有 "isInternal" 字段吗?
如果没有,我们需要加一个。
你:哦对,没有,那加一个吧。
AI:好的,那我们还需要:
- 在用户注册时设置这个字段
- 在搜索时过滤掉 isInternal=true 的用户
- 在用户管理界面加个开关
这些都要做吗?
你:对,都要。
AI:好的,我整理一下需求...
第 3 步:AI 把这些决策记录到 CONTEXT.md
# CONTEXT.md
## 核心概念
### User(用户)
- 系统中的用户账号
- 包含:用户名、邮箱、手机号、是否内部用户
- 内部用户(isInternal=true)不会出现在搜索结果中
### 用户搜索
- 支持按用户名、邮箱模糊搜索
- 每页 20 条结果
- 自动过滤内部用户
- 匹配部分高亮显示
第 4 步:开始写代码,AI 会遵循这些定义
// AI 生成的代码会很清晰
asyncfunctionsearchUsers(query, page = 1) {
constPAGE_SIZE = 20; // 遵循 CONTEXT.md 的定义
returnawait db.users.findMany({
where: {
isInternal: false, // 遵循 CONTEXT.md 的规则
OR: [
{ username: { contains: query } },
{ email: { contains: query } }
]
},
skip: (page - 1) * PAGE_SIZE,
take: PAGE_SIZE
});
}
真实效果对比
不用 /grill-with-docs:
-
来回改了 5 次
-
花了 3 小时
-
代码改得乱七八糟
-
最后还是有遗漏
用了 /grill-with-docs:
-
前 30 分钟在“拷问”
-
写代码只花了 30 分钟
-
一次写对,没有返工
-
还留下了清晰的文档
结论:磨刀不误砍柴工。
技能 2:/tdd —— 让 AI 学会“先想后做”
为什么需要它?
场景:你让 AI 写个登录功能
// AI 写的代码
asyncfunctionlogin(email, password) {
const user = await db.users.findOne({ email });
if (user && user.password === password) {
return { token: generateToken(user) };
}
thrownewError('登录失败');
}
看起来没问题?实际上有 5 个 Bug:
-
密码是明文比对(应该用哈希)
-
邮箱没有转小写(
Test@example.com和test@example.com会被当成两个用户) -
错误信息太笼统(用户不知道是邮箱错了还是密码错了)
-
没有防暴力破解(可以无限次尝试)
-
没有记录登录日志
为什么会这样? 因为 AI 没有反馈,它不知道这些边界情况。
TDD 的核心思想:先写测试,让测试告诉你代码对不对
第 1 轮:最基本的功能
// 先写测试
test('用户可以用正确的邮箱和密码登录', async () => {
// 准备数据
awaitcreateUser({
email: 'test@example.com',
password: 'hashed_password_123'
});
// 执行登录
const result = awaitlogin('test@example.com', 'password123');
// 验证结果
expect(result.token).toBeDefined();
});
// 运行:❌ 失败(因为还没写 login 函数)
// 写最少的代码让测试通过
asyncfunctionlogin(email, password) {
const user = await db.users.findOne({ email });
const isPasswordCorrect = await bcrypt.compare(password, user.passwordHash);
if (isPasswordCorrect) {
return { token: generateToken(user) };
}
thrownewError('登录失败');
}
// 运行:✅ 通过
第 2 轮:处理邮箱大小写
// 写测试
test('邮箱不区分大小写', async () => {
awaitcreateUser({
email: 'test@example.com',
password: 'hashed_password_123'
});
// 用大写邮箱登录
const result = awaitlogin('TEST@EXAMPLE.COM', 'password123');
expect(result.token).toBeDefined();
});
// 运行:❌ 失败
// 改代码
asyncfunctionlogin(email, password) {
const normalizedEmail = email.toLowerCase(); // 加这一行
const user = await db.users.findOne({ email: normalizedEmail });
// ... 其他代码不变
}
// 运行:✅ 通过
第 3 轮:更清晰的错误信息
// 写测试
test('邮箱不存在时应该明确提示', async () => {
awaitexpect(
login('notexist@example.com', 'password123')
).rejects.toThrow('该邮箱未注册');
});
test('密码错误时应该明确提示', async () => {
awaitcreateUser({
email: 'test@example.com',
password: 'hashed_password_123'
});
awaitexpect(
login('test@example.com', 'wrongpassword')
).rejects.toThrow('密码错误');
});
// 运行:❌ 失败
// 改代码
asyncfunctionlogin(email, password) {
const normalizedEmail = email.toLowerCase();
const user = await db.users.findOne({ email: normalizedEmail });
if (!user) {
thrownewError('该邮箱未注册'); // 明确的错误
}
const isPasswordCorrect = await bcrypt.compare(password, user.passwordHash);
if (!isPasswordCorrect) {
thrownewError('密码错误'); // 明确的错误
}
return { token: generateToken(user) };
}
// 运行:✅ 通过
好测试 vs 坏测试
❌ 坏测试:测试实现细节
// 这个测试很脆弱
test('login 函数应该调用 bcrypt.compare', async () => {
const spy = jest.spyOn(bcrypt, 'compare');
awaitlogin('test@example.com', 'password123');
expect(spy).toHaveBeenCalled(); // 测试实现方式
});
// 问题:如果你把 bcrypt 换成 argon2,测试就挂了
// 但实际上功能没变
✅ 好测试:测试行为
// 这个测试很稳定
test('用户可以用正确的密码登录', async () => {
awaitcreateUser({
email: 'test@example.com',
password: 'password123'
});
const result = awaitlogin('test@example.com', 'password123');
expect(result.token).toBeDefined(); // 测试最终结果
});
// 你可以随便换加密算法,只要登录功能正常,测试就过
比喻:
-
坏测试像监工盯着你怎么干活:“你必须先拿锤子,再拿钉子,再敲三下”
-
好测试像验收成果:“我不管你怎么做,钉子钉进去就行”
最常见的错误:水平切片
❌ 错误做法:
// 一口气写 10 个测试
test('测试 1', ...);
test('测试 2', ...);
test('测试 3', ...);
// ... 写了 10 个
// 然后开始写实现
functionlogin() {
// 写了 100 行代码
}
// 结果:发现前 5 个测试都写错了,要全部重写
✅ 正确做法:垂直切片
// 写 1 个测试
test('基本登录', ...);
// 写实现
functionlogin() {
// 10 行代码
}
// 测试通过,继续
// 写第 2 个测试
test('邮箱大小写', ...);
// 改实现
functionlogin() {
// 加 1 行代码
}
// 测试通过,继续...
比喻:
-
水平切片:先画 10 层楼的图纸,再一口气盖楼 → 盖到一半发现图纸画错了
-
垂直切片:盖一层,检查一层,再盖下一层 → 每一层都是对的
技能 3:/diagnose —— 系统化调试,不再“瞎试”
为什么需要它?
典型的调试过程:
你:这个功能报错了
AI:我看看... 可能是这里的问题,我改一下
你:还是报错
AI:那可能是那里的问题,我再改一下
你:还是报错
AI:要不试试这样?
你:(崩溃)
问题在哪? 没有方法,纯靠猜。就像看病不做检查,直接开药。
系统化调试的三大步骤
步骤 1:建立反馈循环(最重要!)
核心思想: 在修 Bug 之前,先建立一个能稳定重现 Bug 的方法。
场景:用户反馈“有时候登录会失败”
❌ 错误做法:
你:AI,用户说登录有时候会失败,你看看哪里有问题
AI:(看了代码)可能是密码验证的问题,我改一下
你:好像还是会失败...
AI:那可能是数据库连接的问题...
✅ 正确做法:先建立反馈循环
// 第 1 步:写一个能重现 Bug 的测试
test('重现登录失败的 Bug', async () => {
// 模拟用户的操作
awaitcreateUser({ email: 'test@example.com', password: 'pass123' });
// 尝试登录 100 次,看看会不会失败
for (let i = 0; i < 100; i++) {
const result = awaitlogin('test@example.com', 'pass123');
expect(result.token).toBeDefined();
}
});
// 运行:❌ 失败了 3 次
// 现在你有了一个能稳定重现 Bug 的方法!
为什么这一步最重要?
-
没有反馈循环:改代码 → 手动测试 → 不知道对不对 → 再改 → 再测……(可能花 2 小时)
-
有反馈循环:改代码 → 运行测试 → 2 秒知道结果 → 再改 → 2 秒知道结果……(可能花 20 分钟)
比喻:
-
没有反馈循环:蒙着眼睛扔飞镖,扔完了走过去看中没中
-
有反馈循环:每扔一次飞镖,立刻知道中没中
建立反馈循环的 10 种方法:
-
写个失败的测试(最常用)
-
写个 curl 脚本(适合 API)
# test-login.sh
curl -X POST http://localhost:3000/login \
-d '{"email":"test@example.com","password":"pass123"}'
-
写个 CLI 命令(适合命令行工具)
# 运行 100 次,看看哪次会失败
for i in {1..100}; do
node cli.js login test@example.com pass123
done
-
用 Playwright 写个浏览器脚本(适合前端)
test('登录测试', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.fill('[name=email]', 'test@example.com');
await page.fill('[name=password]', 'pass123');
await page.click('button[type=submit]');
awaitexpect(page).toHaveURL('/dashboard');
});
步骤 2:提出假设并验证
现在你有了反馈循环,可以快速验证假设了。
你:登录有时候会失败
AI:我生成 3 个假设:
假设 1:并发问题
- 预测:如果是并发问题,单线程测试应该不会失败
- 验证:写个单线程测试
- 结果:单线程也会失败 ❌ 假设错误
假设 2:数据库连接池耗尽
- 预测:如果是连接池问题,增加连接数应该能解决
- 验证:把连接池从 10 改成 100
- 结果:还是会失败 ❌ 假设错误
假设 3:bcrypt 在某些情况下比对失败
- 预测:如果是 bcrypt 的问题,加日志应该能看到
- 验证:在 bcrypt.compare 前后加日志
- 结果:发现有时候 user.passwordHash 是 undefined!✅ 找到了!
原因:创建用户时,如果密码为空,passwordHash 会是 undefined
修复:在创建用户时加上密码非空验证
关键点:一次只验证一个假设
❌ 错误做法:
同时改了 3 个地方,测试通过了,但不知道是哪个改对了
✅ 正确做法:
改一个地方 → 测试 → 没通过 → 撤销
改另一个地方 → 测试 → 通过了 → 找到原因了!
步骤 3:修复并防御
// 第 1 步:写个回归测试(防止以后再犯)
test('创建用户时密码不能为空', async () => {
awaitexpect(
createUser({ email: 'test@example.com', password: '' })
).rejects.toThrow('密码不能为空');
});
// 第 2 步:修复代码
asyncfunctioncreateUser({ email, password }) {
if (!password) {
thrownewError('密码不能为空'); // 加上验证
}
const passwordHash = await bcrypt.hash(password, 10);
returnawait db.users.create({ email, passwordHash });
}
// 第 3 步:验证原来的 Bug 是否修复
test('登录不再随机失败', async () => {
awaitcreateUser({ email: 'test@example.com', password: 'pass123' });
// 登录 100 次,应该全部成功
for (let i = 0; i < 100; i++) {
const result = awaitlogin('test@example.com', 'pass123');
expect(result.token).toBeDefined();
}
});
// 运行:✅ 全部通过!
真实效果对比
不用 /diagnose:
-
花了 3 小时
-
改了 10 个地方
-
不确定是哪个改对的
-
可能引入了新 Bug
用了 /diagnose:
-
花了 30 分钟
-
改了 1 个地方
-
确定是这个问题
-
加了测试,以后不会再犯
技能 4:/improve-codebase-architecture —— 代码“体检”
为什么需要它?
项目的生命周期:
第 1 个月:代码很清晰,改起来很快
第 3 个月:开始有点乱,但还能改
第 6 个月:改一个地方要动 5 个文件
第 9 个月:不敢改了,怕改坏
第 12 个月:重构吧,这坨代码没救了
问题: AI 能让你快速写代码,但也能快速制造混乱。
解决方案: 定期“体检”,及早发现问题。
什么是“浅层模块”?用生活例子来理解
先说个生活中的例子:
你要做一顿饭,有两种方式:
方式 A:简单直接
1. 打开冰箱拿食材
2. 切菜
3. 炒菜
4. 装盘
方式 B:过度复杂
1. 找到"冰箱管理员",告诉他你要什么食材
2. "冰箱管理员"再去找"食材定位专家"
3. "食材定位专家"找到食材后,交给"食材搬运工"
4. "食材搬运工"把食材交给你
5. 你再开始切菜...
方式 B 就是“浅层模块”的典型表现——看起来很专业,实际上把简单的事情搞复杂了。
代码中的真实案例
场景:你让 AI 做了个订单结账功能
第 1 版:简单清晰(3 个月前)
classOrder {
constructor(items, user) {
this.items = items; // 订单商品
this.user = user; // 下单用户
// 直接计算总价
this.total = items.reduce((sum, item) => sum + item.price, 0);
}
asynccheckout() {
awaitthis.processPayment(); // 处理支付
awaitthis.sendConfirmation(); // 发送确认邮件
returnthis;
}
}
// 使用:3 行代码搞定
const order = newOrder(cartItems, currentUser);
await order.checkout();
console.log('订单完成!');
为什么这个版本好?
-
一眼就能看懂在做什么
-
要改逻辑,直接在 Order 类里改
-
没有多余的东西
第 20 版:复杂混乱(AI “优化”了 20 次后)
classOrder {
constructor(items, user) {
this.items = items;
this.user = user;
// 注意:总价不在这里算了
}
asynccheckout() {
// 步骤 1:验证订单
const validator = newOrderValidator(this);
const errors = validator.validate();
if (errors.length > 0) {
thrownewValidationError(errors);
}
// 步骤 2:计算总价(为什么要单独一个类?)
const calculator = newPriceCalculator(this.items);
this.total = calculator.calculate();
// 步骤 3:处理支付(工厂模式,但只有一种支付方式)
const paymentProcessor = PaymentProcessorFactory.create(this.paymentMethod);
const result = await paymentProcessor.process(this);
// 步骤 4:发送通知(又一个工厂)
const notifier = NotifierFactory.create(this.user.preferredChannel);
await notifier.send(this);
returnthis;
}
}
// 使用:看起来一样,但背后多了 5 个类
const order = newOrder(cartItems, currentUser);
await order.checkout();
console.log('订单完成!');
问题在哪?让我们逐个分析:
问题 1:OrderValidator —— 假装在验证
// OrderValidator 的实现
classOrderValidator {
constructor(order) {
this.order = order;
}
validate() {
const errors = [];
// 就做了这么点事:
if (this.order.items.length === 0) {
errors.push('订单不能为空');
}
this.order.items.forEach(item => {
if (item.price <= 0) {
errors.push('商品价格必须大于 0');
}
});
return errors;
}
}
// 问题:这 10 行代码为什么要单独一个类?
// 直接写在 checkout() 里不是更清楚吗?
这就像: 你要检查冰箱里有没有鸡蛋,结果专门雇了个“鸡蛋检查员”,他的工作就是打开冰箱看一眼。
问题 2:PriceCalculator —— 一行代码的类
// PriceCalculator 的实现
classPriceCalculator {
constructor(items) {
this.items = items;
}
calculate() {
// 就这一行!
returnthis.items.reduce((sum, item) => sum + item.price, 0);
}
}
// 使用:
const calculator = newPriceCalculator(this.items);
this.total = calculator.calculate();
// 问题:为什么不直接写?
this.total = this.items.reduce((sum, item) => sum + item.price, 0);
这就像: 你要算 1+1,结果专门买了个计算器,还要先看说明书。
问题 3:PaymentProcessorFactory —— 只有一个选项的“工厂”
// PaymentProcessorFactory 的实现
classPaymentProcessorFactory {
staticcreate(method) {
// 现在只支持 Stripe
if (method === 'stripe') {
returnnewStripeProcessor();
}
// 以后可能支持 PayPal、支付宝...
// 但现在还没有!
}
}
// 使用:
const processor = PaymentProcessorFactory.create('stripe');
await processor.process(order);
// 问题:现在只有一种支付方式,为什么要工厂?
// 直接用不就行了?
const processor = newStripeProcessor();
await processor.process(order);
这就像: 你家只有一辆车,结果建了个“车辆调度中心”,每次开车都要先去调度中心申请。
总结:什么是“浅层模块”?
|
|
|
|
|---|---|---|
| 接口 |
|
|
| 功能 |
|
|
| 价值 |
|
|
| 比喻 |
|
|
判断方法:删除测试
-
删掉后,代码变简单了 → 浅层模块,应该删
-
删掉后,复杂度分散到 10 个地方 → 深层模块,应该留
“删除测试”:判断模块有没有价值
方法:想象删掉这个模块,会发生什么?
测试 OrderValidator:
// 删掉 OrderValidator
classOrder {
asynccheckout() {
// 把验证逻辑直接写在这里
if (this.items.length === 0) {
thrownewError('订单不能为空');
}
for (const item ofthis.items) {
if (item.price <= 0) {
thrownewError('商品价格必须大于 0');
}
}
// ... 其他逻辑
}
}
// 结果:代码更清晰了!
// 结论:OrderValidator 是个"浅层模块",应该删掉
测试 PriceCalculator:
// 删掉 PriceCalculator
classOrder {
asynccheckout() {
// 直接计算总价
this.total = this.items.reduce((sum, item) => sum + item.price, 0);
// ... 其他逻辑
}
}
// 结果:从 3 行变成 1 行,更清晰了!
// 结论:PriceCalculator 也应该删掉
但是,有些模块不能删:
// 假设你有个 EmailService
classEmailService {
asyncsend(to, subject, body) {
// 连接 SMTP 服务器
// 处理认证
// 发送邮件
// 处理错误重试
// 记录日志
// ... 100 行代码
}
}
// 如果删掉 EmailService,这 100 行代码会散落到 10 个地方
// 结论:EmailService 是个"深层模块",应该保留
判断标准:
-
浅层模块:删掉后,复杂度消失了 → 应该删
-
深层模块:删掉后,复杂度分散到 N 个地方 → 应该留
真实案例:重构一个“浅层模块”
Before:
// 有个 UserProfileFormatter 类
classUserProfileFormatter {
constructor(user) {
this.user = user;
}
format() {
return {
name: this.user.name,
email: this.user.email,
avatar: this.user.avatar || '/default-avatar.png'
};
}
}
// 用起来很麻烦
const formatter = newUserProfileFormatter(user);
const profile = formatter.format();
After:
// 直接在 User 类里加个方法
classUser {
toProfile() {
return {
name: this.name,
email: this.email,
avatar: this.avatar || '/default-avatar.png'
};
}
}
// 用起来很简单
const profile = user.toProfile();
好处:
-
少了一个类
-
代码更清晰
-
更符合直觉(用户的资料,当然应该是用户自己提供)
什么时候该抽象?
原则:有两个以上的使用场景,再抽象
❌ 过度抽象:
// 现在只有一种支付方式,就搞了个工厂
classPaymentProcessorFactory {
staticcreate(method) {
if (method === 'stripe') {
returnnewStripeProcessor();
}
// 以后可能支持 PayPal、支付宝...
}
}
// 问题:现在只有 Stripe,这个工厂没有意义
✅ 合理抽象:
// 现在只有一种支付方式,直接用
classStripeProcessor {
asyncprocess(order) {
// 处理支付
}
}
// 等有第二种支付方式时,再抽象
// 到时候你会更清楚应该怎么抽象
比喻:
-
过度抽象:你家只有一辆车,就建了个车库管理系统
-
合理抽象:等你有了第二辆车,再考虑怎么管理
如何开始使用
安装
-
访问 github.com/mattpocock/skills
-
运行
skills.sh安装程序 -
选择你需要的技能和目标 AI 工具
-
在项目中运行
/setup-matt-pocock-skills初始化
实践路线图
新项目(推荐路线)
第 1 天:建立基础
1. 运行 /setup-matt-pocock-skills
2. 创建 CONTEXT.md,写下核心概念
3. 用 /grill-with-docs 理清第一个功能
第 1 周:养成习惯
1. 每次做新功能前,先 /grill-with-docs
2. 关键功能用 /tdd 写测试
3. 遇到 Bug 用 /diagnose
第 1 个月:定期体检
1. 每周五运行 /improve-codebase-architecture
2. 发现问题及时重构
3. 更新 CONTEXT.md
现有项目(救火路线)
第 1 步:止血(1 天)
1. 找出最痛的 3 个问题
2. 用 /diagnose 修复最严重的 Bug
3. 用 /improve-codebase-architecture 找出最乱的模块
第 2 步:建立文档(1 周)
1. 创建 CONTEXT.md
2. 写下核心概念和术语
3. 记录重要的架构决策(ADR)
第 3 步:逐步改善(持续)
1. 新功能用 /grill-with-docs + /tdd
2. 改老代码时顺手重构
3. 每周运行 /improve-codebase-architecture
团队推广建议
第 1 阶段:个人试用(1 周)
-
你自己先用起来
-
记录效果(节省了多少时间、避免了多少 Bug)
-
准备案例
第 2 阶段:小范围推广(1 个月)
-
找 2-3 个愿意尝试的同事
-
一起用,互相反馈
-
完善团队的 CONTEXT.md
第 3 阶段:全团队推广(持续)
-
在团队会议上分享效果
-
建立团队规范(什么时候用哪个技能)
-
定期回顾和改进
常见问题
Q1:这些技能会不会让开发变慢?
短期: 是的,前期会慢一点(多花 20-30% 时间)
长期: 不会,反而会更快
真实数据(来自使用者反馈):
-
第 1 周:慢 30%(在学习和适应)
-
第 2 周:慢 10%(开始习惯)
-
第 1 个月:持平(速度恢复)
-
第 3 个月:快 50%(因为代码质量好,改起来快)
-
第 6 个月:快 2 倍(因为没有技术债务)
比喻:
-
不用这些技能:像开快车不系安全带,一开始很快,但迟早出事
-
用这些技能:像开车系安全带,一开始慢 2 秒,但能安全到达
Q2:我的项目很小,需要用这些吗?
需要! 尤其是小项目。
原因:
-
小项目更容易变成大项目
-
小项目的坏习惯会带到大项目
-
在小项目上练习,成本更低
建议:
-
小项目:至少用
/grill-with-docs和CONTEXT.md -
中项目:加上
/tdd -
大项目:全套都用
Q3:AI 会自动遵循这些技能吗?
不会。 你需要主动调用。
但是:
-
用几次后,AI 会学习你的模式
-
CONTEXT.md会让 AI 自动遵循你的术语 -
测试会自动约束 AI 的行为
建议:
-
把常用技能做成快捷键或命令
-
在项目 README 里写清楚什么时候用哪个技能
-
团队成员互相提醒
Q4:这些技能适合所有编程语言吗?
是的。 这些是方法论,不是具体的工具。
已验证的语言:
-
JavaScript / TypeScript
-
Python
-
Go
-
Rust
-
Java
-
C#
核心思想是通用的:
-
需求对齐(任何语言都需要)
-
共享语言(任何项目都需要)
-
测试驱动(任何语言都能做)
-
代码设计(任何语言都重要)
核心价值观
这套技能体系的设计哲学:
1. 小而美,不是大而全
不是: 一个包罗万象的框架而是: 一组可以灵活组合的小工具
比喻:
-
不是瑞士军刀(100 个功能,每个都不好用)
-
而是专业工具箱(每个工具专注做好一件事)
2. 授人以渔,不是授人以鱼
不是: 让 AI 替你做决策而是: 教你如何与 AI 协作
比喻:
-
不是自动驾驶(你坐着就行)
-
而是驾驶辅助(你还是司机,但有了助手)
3. 基于经验,不是赶时髦
不是: 最新最炫的技术而是: 经过几十年验证的原则
来源:
-
《程序员修炼之道》(1999 年)
-
《领域驱动设计》(2003 年)
-
《软件设计哲学》(2018 年)
-
《极限编程解析》(1999 年)
这些书都是经典,原则不会过时。
4. 适配 AI,不是对抗 AI
不是: “AI 不可靠,少用为妙”而是: “AI 很强大,但需要正确使用”
比喻:
-
不是拒绝用电动工具(“还是手工锯靠谱”)
-
而是学会安全使用电动工具(“戴好护目镜,抓稳了”)
5. 持续改进,不是一劳永逸
不是: “设置一次,永久有效”而是: “定期体检,及时调整”
建议:
-
每周回顾:哪些技能用得好?哪些还需要改进?
-
每月更新:
CONTEXT.md是否需要补充?ADR 是否需要添加? -
每季度优化:运行
/improve-codebase-architecture,大扫除
结语:AI 是工具,你才是工程师
三年前,我们担心 AI 会取代程序员。
现在我们知道了:AI 不会取代程序员,但会用 AI 的程序员会取代不会用的。
但这里有个更深的问题:会用 AI ≠ 让 AI 替你写代码
真正会用 AI 的程序员,是那些:
-
知道什么时候该让 AI 写代码
-
知道什么时候该让 AI 闭嘴
-
知道如何引导 AI 写出好代码
-
知道如何验证 AI 写的代码
这套技能体系,就是教你这些的。
最后,送你一句话:
“工具会变,原则不变。AI 会进化,工程思维永恒。
不要做 AI 的打字员,要做 AI 的架构师。“
相关资源:
-
GitHub 仓库:github.com/mattpocock/skills
-
Matt Pocock 的 Newsletter:aihero.dev(6 万+ 开发者订阅)
开始行动:
-
Star 这个仓库
-
选一个小项目试试
-
一周后,回来告诉我效果如何
你的经验也很重要:
-
你在用 AI 编程时遇到过哪些坑?
-
你有什么实践心得?
-
欢迎在评论区分享!
本文基于 Matt Pocock 的开源项目深度解读,致敬所有为工程实践贡献智慧的开发者。
夜雨聆风