我三个月不敢碰的那段代码
在 src/services/restaurant.service.ts 里,有一个 847 行的函数。名字叫 getRestaurantsWithEverything。
它做的事情从名字就看出来了:"把所有的东西都查出来"。
async function getRestaurantsWithEverything(
params: GetRestaurantsParams
): Promise<RestaurantWithEverything> {
// 筛选条件:距离、评分、人均、菜系、标签、是否营业……
// 关联查询:评论、点赞、收藏、优惠券、套餐、预约……
// 排序逻辑:距离优先/评分优先/综合排序/人气排序……
// 缓存逻辑:Redis 缓存 5 分钟,带版本号……
// 总计:847 行
}
每次新加一个平台功能,比如"筛选支持宠物友好的餐厅",我就往这个函数里加 20 行。
三个月后,它变成了一个我完全不敢动的怪兽。
为什么不敢重构
1. 没有测试
这 847 行的函数没有一行测试。因为它做的事情太多了——它不光查数据库,还调 Redis,还调搜索引擎,还打埋点。写单元测试要 mock 一整个宇宙。
2. 不知全貌
三个月里我至少改了它 30 次。每次都加一点、修一点。现在的我,已经不记得这个函数的所有分支了。重构它可能引入的 bug,比我修过的 bug 加起来还多。
3. 牵一发而动全身
六个平台都在用这个函数。我在觅食猫上改筛选逻辑,可能会莫名其妙地影响搬家通的排序。
源码依赖关系:
restaurant.service.ts ← 觅食猫
← 本地生活
← 同城推荐
← 商家入驻
← 优惠券平台
← 搬家通(???为什么会依赖这个)
搬家通是一个搬家平台,它为什么会依赖餐厅服务?因为三个月前偷懒,搬家通的首页用了餐厅服务里的广告位接口。
这个依赖关系没有任何文档,它活在我一个人的脑子里。
下决心重构
一天早上,觅食猫出了个 bug:筛选"人均 100 以下"的餐厅,结果里出现了人均 300 的米其林。原因是 30 天前加的新标签功能影响了价格筛选的 where 条件。
我终于受不了了。要么重构,要么以后每次修 bug 都像拆弹。
重构策略:不是重写,是重构
第一步:添加测试——先造安全网
重构之前必须先有测试。不是"重构后再测试",是先测试,再重构。
// src/services/__tests__/restaurant.service.test.ts
describe('RestaurantService', () => {
describe('filterByPrice', () => {
it('should filter out restaurants above max price', async () => {
const result = await filterByPrice(allRestaurants, { maxPrice: 100 })
expect(result.every(r => r.avgPrice <= 100)).toBe(true)
})
it('should not affect other filters', async () => {
const result = await filterByPrice(ratedRestaurants, { maxPrice: 100 })
// 评分过滤仍然有效
expect(result.every(r => r.avgPrice <= 100 && r.rating >= 4.0)).toBe(true)
})
})
// 每条查询逻辑写一条测试
// 31 个测试函数,覆盖所有已知分支
})
花了一整天写测试,写完了 31 个测试函数。其中 4 个测试直接失败了——它们暴露了我之前修过的 bug 但测试条件不对。先修正测试,再开始重构。
第二步:拆分——一个函数只做一件事
// ❌ 重构前:一个 847 行的怪物
getRestaurantsWithEverything()
// 什么都做,什么都做不好
// ✅ 重构后:每个函数职责单一
getRestaurants(filters: FilterParams): Promise<Restaurant[]>
enrichWithRatings(restaurants: Restaurant[]): Promise<EnrichedRestaurant[]>
enrichWithPromotions(restaurants: EnrichedRestaurant[]): Promise<FullRestaurant[]>
applySorting(restaurants: FullRestaurant[], sortBy: SortType): FullRestaurant[]
buildResponse(restaurants: FullRestaurant[]): RestaurantResponse
拆分后的函数最长不超过 80 行。每个函数都能独立测试。
第三步:提取策略模式——消除 if-else 地狱
排序逻辑最初是一大坨 if-else:
// ❌ 重构前
if (sortBy === 'distance') {
result.sort((a, b) => a.distance - b.distance)
} else if (sortBy === 'rating') {
result.sort((a, b) => b.rating - a.rating)
} else if (sortBy === 'popularity') {
result.sort((a, b) => b.views - a.views)
}
// ... 还有 5 种排序方式
// ✅ 重构后:策略模式
const sortStrategies: Record<SortType, (a: FullRestaurant, b: FullRestaurant) => number> = {
distance: (a, b) => a.distance - b.distance,
rating: (a, b) => b.rating - a.rating,
popularity: (a, b) => b.views - a.views,
avgPrice: (a, b) => a.avgPrice - b.avgPrice,
newest: (a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
relevance: (a, b) => calculateRelevance(b) - calculateRelevance(a),
}
function applySorting(restaurants: FullRestaurant[], sortBy: SortType) {
return [...restaurants].sort(sortStrategies[sortBy])
}
新增排序方式只需要在 sortStrategies 里加一行。
第四步:清理依赖——断开不该有的连线
// 搬家通不应该依赖餐厅服务
// 提取共享逻辑到独立模块
// src/lib/common/ad-slots.ts ← 新文件
export function getAdSlots(platform: PlatformType, context: AdContext) {
// 通用的广告位逻辑,各个平台独立调用
}
重构后:搬家通只依赖 ad-slots.ts,不再依赖整个餐厅服务。依赖关系清晰了。
重构结果
| 指标 | 重构前 | 重构后 | 变化 |
|---|---|---|---|
| 最大函数行数 | 847 | 78 | -91% |
| 文件数量 | 1 个文件 | 8 个文件 | 解耦 |
| 测试覆盖率 | 0% | 78% | 从无到有 |
| 新增筛选条件 | ~30 分钟 | ~5 分钟 | -83% |
| 跨平台影响 | 不可控 | 可控 | 依赖清晰 |
| bug 修复时间 | 1-4 小时 | 10-30 分钟 | -70% |
最重要的是:我现在敢改这段代码了。 改完后跑一遍测试,31 个绿灯全部亮起,心里就有底。
写在最后
重构这件事,道理大家都懂:"Don't repeat yourself""单一职责""高内聚低耦合"。但真正下决心去做,需要一次刻骨铭心的痛。
对我来说,那次"人均 100 以下出现米其林"的 bug,就是那最后一根稻草。847 行的函数不会明天就爆炸,但它会持续消耗你的开发效率、你的改 bug 速度、你的睡眠质量。
你今天为了"快点上线"写的每一行代码,都在为三个月后的自己埋炸弹。
现在我的开发流程里多了一条硬性规定:任何函数超过 150 行必须拆分,超过 300 行视为技术债务,计入 Sprint 工时。
三个月前那个不敢改代码的自己,如果看到现在的代码,应该会松一口气吧。