三大“屎山”代码重构技巧
技巧一:“考古式”重构 —— 先画图、后动刀
核心思想:面对陌生的老代码,不要急着删改,先把自己变成“代码考古学家”,理清业务逻辑。
具体做法
1. 建立“代码地图”
打开项目后,不要直接跳进那个2000行的“怪物组件”。先用思维导图工具(Xmind、ProcessOn)或白板,把代码里的逻辑分支画出来。
操作步骤:
- 从入口文件开始,逐行阅读
- 遇到一个条件分支(if/else),就在图上画一个分叉
- 遇到一个API调用,记下接口地址和参数
- 遇到一个状态变更,追踪它的流向
实战示例:
假设你接手的是一个用户管理组件,阅读代码后画出这样的图:
用户管理组件
├── 初始化
│ ├── 获取用户列表 (API: /api/users)
│ ├── 获取角色列表 (API: /api/roles)
│ └── 获取权限配置 (API: /api/permissions)
├── 交互逻辑
│ ├── 搜索
│ │ ├── 按姓名搜索 → 调用搜索接口
│ │ └── 按角色筛选 → 前端过滤
│ ├── 新增用户
│ │ ├── 弹窗表单
│ │ ├── 表单校验
│ │ └── 提交接口
│ └── 编辑用户
│ ├── 权限判断 (admin可编辑)
│ ├── 回填表单
│ └── 提交更新
└── 异常处理
├── 网络错误 → 显示重试按钮
├── 无权限 → 隐藏操作按钮
└── 数据为空 → 显示空状态
2. 标记“可疑地带”
在代码中埋下注释标记,比如:
// TODO[重构笔记]: 这里发了3个请求,但只有最后一个数据被使用
// TODO[重构笔记]: 这个函数长达200行,做了数据获取、格式化、权限判断
// TODO[重构笔记]: 这个变量名 'data' 到底存的是什么?看了10分钟没看懂
3. 写“考古报告”
在动手重构前,先写一篇简短的文档,回答三个问题:
- 这个组件/模块到底是干什么的?
- 它有哪些业务场景?
- 目前的代码结构有哪些问题?
为什么要先画图?
因为看不懂就不能重构。很多重构失败的案例,都是因为没搞懂业务逻辑,改完代码后发现功能不全了。画图的过程,就是你把业务逻辑从代码里“提取”出来的过程。
技巧二:“外科手术式”重构 —— 一次只动一个点
核心思想:重构不是重写,要像做外科手术一样精准、微创,每次改动只解决一个问题。
具体做法
1. 制定“手术方案”
把一个大问题拆解成多个小任务。比如那个2000行的组件,可以拆成这样:
| 手术顺序 | 手术目标 | 预计耗时 | 风险等级 |
|---|---|---|---|
| 第一刀 | 提取API请求到独立的service文件 | 2小时 | ⭐(低) |
| 第二刀 | 提取权限判断逻辑到自定义Hook | 3小时 | ⭐⭐ |
| 第三刀 | 拆分render函数,提取子组件 | 4小时 | ⭐⭐⭐ |
| 第四刀 | 用策略模式重构那500行if/else | 1天 | ⭐⭐⭐⭐ |
2. 开启“手术模式”
每台“手术”遵循相同的流程:
- 术前准备:确保当前代码是可运行状态,commit一次(哪怕代码很烂,先commit)
- 手术操作:只改动目标范围内的代码,其他代码碰都不要碰
- 术中测试:改完一点,立即测试相关功能是否正常
- 术后缝合:运行整个项目,确保没有引入新的bug
- 提交记录:commit,并写下清晰的commit message
实战示例:提取API请求
// 重构前:组件里直接发请求
const UserManage = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []);
// 还有2000行其他代码...
}
// 重构后:第一步,只把请求移出去
// services/userService.js
export const userService = {
getUsers: () => fetch('/api/users').then(res => res.json()),
// 其他API方法...
}
// components/UserManage.jsx
import { userService } from '../services/userService';
const UserManage = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
userService.getUsers().then(setUsers);
}, []);
// 其他代码暂时不动
}
3. 使用“手术器械”
善用IDE的重构工具:
- VS Code:选中代码 → 右键 → 重构提取(提取函数、提取常量、提取变量)
- 快捷键:
Ctrl+Shift+R调出重构菜单
4. 留下“手术记录”
每次重构完成后,在commit message里写清楚做了什么:
git commit -m "refactor: 提取用户管理模块的API请求到独立service
- 创建 userService.js,封装所有用户相关接口
- 移除组件内直接调用fetch的代码
- 为后续单元测试做准备
- 功能无变化,所有接口测试通过"
为什么要“一次只动一个点”?
因为可逆才能可控。如果一次改了100个文件,出问题了都不知道回滚到哪。但如果你只改了1个文件,出了问题,git checkout一下就回来了。
技巧三:“以终为始”重构 —— 让代码为未来而生
核心思想:重构不只是让代码变好看,而是让代码更容易应对未来的需求变化。站在未来看现在,你的代码需要具备什么样的能力?
具体做法
1. 识别“未来可能变”的点
根据业务经验,预判哪些地方未来最可能改动:
- 业务规则:比如优惠计算方式、权限校验逻辑
- UI表现:比如主题切换、组件样式
- 数据来源:比如从REST API换成GraphQL
- 第三方依赖:比如换一个图表库
2. 应用“面向未来”的设计模式
针对不同场景,选择合适的设计模式:
场景一:大量的if/else判断业务类型
// 重构前:每次加新类型都要改这里
const getPrice = (type, basePrice) => {
if (type === 'student') {
return basePrice * 0.8;
} else if (type === 'member') {
return basePrice * 0.9;
} else if (type === 'vip') {
return basePrice * 0.7;
} else if (type === 'seasonal') {
return basePrice * 0.5; // 还要判断季节...
}
// 未来还会有更多类型
}
// 重构后:策略模式,开闭原则
const priceStrategies = {
student: (price) => price * 0.8,
member: (price) => price * 0.9,
vip: (price) => price * 0.7,
// 未来加新类型,只需要在这里加一个策略
}
const getPrice = (type, basePrice) => {
const strategy = priceStrategies[type] || ((price) => price);
return strategy(basePrice);
}
场景二:组件里混杂各种UI状态
// 重构前:一个组件处理loading、error、empty、normal
const UserList = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState([]);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
if (data.length === 0) return <Empty />;
return <List data={data} />;
}
// 重构后:状态驱动,未来加新状态(比如网络重连)很容易
const USER_LIST_STATES = {
LOADING: 'loading',
ERROR: 'error',
EMPTY: 'empty',
SUCCESS: 'success'
}
const UserList = () => {
const [state, setState] = useState(USER_LIST_STATES.LOADING);
const [data, setData] = useState([]);
const stateComponents = {
[USER_LIST_STATES.LOADING]: <Spinner />,
[USER_LIST_STATES.ERROR]: <Error onRetry={fetchData} />,
[USER_LIST_STATES.EMPTY]: <Empty />,
[USER_LIST_STATES.SUCCESS]: <List data={data} />
}
return stateComponents[state] || null;
}
3. 建立“代码防腐层”
对于那些你不能改(比如第三方SDK、后端接口)但可能会变的依赖,加一层防腐:
// 重构前:直接使用第三方库
// 如果哪天这个库不维护了,换库要改几百个文件
const result = moment(date).format('YYYY-MM-DD');
// 重构后:封装一层,隔离变化
// utils/dateUtil.js
import moment from 'moment';
export const formatDate = (date, format = 'YYYY-MM-DD') => {
return moment(date).format(format);
}
// 如果以后要换成day.js,只需要改这一个文件
4. 编写“未来可读”的代码
- 命名即注释:不要用
data、res、temp,用userList、apiResponse、formattedPrice - 复杂度治理:一个函数超过50行?拆!一个组件超过300行?拆!
- 依赖显式化:不要用隐式的全局状态,用props、context、hooks显式传递
把这三大技巧写成文章的配图建议
为了让文章更生动,建议配这几张图:
- 技巧一的配图:一张手绘风格的思维导图,展示那个2000行组件被拆解后的逻辑分支,旁边放一个拿着放大镜的小人(考古学家)
- 技巧二的配图:一张手术室的照片风格图,把代码文件画成躺在手术台上的病人,旁边放一排“手术器械”(函数提取、变量提取、组件拆分等工具)
- 技巧三的配图:一张时间轴,左边是过去(屎山代码),中间是现在(重构中),右边是未来(新需求轻松接入),画几条线从“未来”指向代码的不同模块,表示这些地方预留了扩展点
- 封面图建议:用Canva做一张图,标题《3个技巧,让老代码起死回生》,背景放一段模糊的乱码代码,前景放一个干净整洁的代码片段,中间画一个箭头和“重构”二字
把这三大技巧写成文章的段落标题建议
| 技巧 | 段落小标题建议 |
|---|---|
| 技巧一 | “看不懂的代码不要改”:我是怎么给屎山画CT片的 |
| 技巧二 | “一次只动一个点”:外科手术式的精准重构 |
| 技巧三 | “为未来而重构”:让代码长出应对变化的能力 |
这三个技巧层层递进:先看懂(考古)→ 再动手(手术)→ 最后升华(面向未来)