大家好,我是宝哥。
代码重构,说白了就是给代码做“美容”。但如果操作不当,后果可能很严重,就像装修房子,翻新系统,稍有不慎就会变成“拆迁现场”。
每次重构都暗藏风险
代码改进,说到底就是改动一个正在运行的系统,风险是不可避免的。但我们可以通过一些方法来降低风险,让重构变得可控。
有些重构任务很大,涉及很多系统,而有些则只局限在一个小地方,但可能会意外地影响到其他部分,甚至导致关键业务中断,比如重要的购买流程。还有第三种情况,就是为了新功能“腾出空间”进行的改进,例如,将单一产品购买流程改为支持多种商品,然后才添加新商品。
这三种情况都有一个共同点:高风险!
- 如果操作失误,会导致业务损失、客户流失、团队成员士气低落,甚至影响新功能的开发。
- 另一方面,重构需要额外的时间、精力和经验丰富的开发者,成本不低。
应对风险:清单
那么,如何有效应对重构的风险呢?
- 定义边界: 明确这次重构的范围,不要贪多,避免出现不可控的局面。
- 隔离改进与新功能: 不要同时进行重构和新功能开发,避免彼此影响。
- 编写全面测试: 测试应该涵盖更多层级,包括集成测试,尽量少涉及实现细节。
- 进行视觉确认: 在浏览器中打开页面,仔细检查变化是否符合预期。
错误的做法:
- 不要跳过测试: 测试是重构的关键步骤,不能为了省事而省略。
- 不要过度依赖代码审查和 QA: 人都会犯错,不要把全部希望寄托于他们。
- 不要把重构和清理混在一起: 可以将一些小的代码改进与其他更改结合,但大型重构最好单独进行。
有时,你需要重构的代码位置和内容都很明确,例如将一些代码从组件中移出,或者进行一些代码清理,以便新功能不会在“破碎的玻璃”上运行。对于开发者来说,这可能看起来是件小事,但挑战性的重构(尤其是)隐藏着陷阱。由于开发环境障碍、依赖关系、数据库/ API 问题、不稳定的测试或缺乏时间等原因,验证更改并不容易。整个重构过程中,经常出现问题。如何尽早发现问题呢?
等等!
在开始重构之前,你需要先思考以下几点:
- 评估风险: 从开发和业务角度评估重构的风险(成本)。如果重构失败,会带来哪些负面影响?
- 独立任务还是功能的一部分: 重构是独立的任务,还是为了新功能而进行的?
- 验证系统是否正常工作: 在开始重构之前,确保所有相关部分都在正常运行。
重构是件大事
重构的关键在于:确保重构完成后系统依然正常工作! 为了做到这一点,需要编写全面的测试,并贯穿整个重构过程。
使用测试形式的面包屑:
- 编写单元测试和集成测试。
- 测试应该尽量避免实现细节。
- 编写尽可能多的测试,以建立信心。
集成测试在捕捉组件副作用方面非常实用。
// React 组件测试
it('应该显示所有插件', () => {
// 模拟服务器响应
// 利用尽可能低级别的模拟工具 — window.fetch()
server.get(endpoints.loadAddonsData, { addons: ['addon1', 'addon2'] });
// 控制组件按钮的属性
props.shouldShowMoreAddons = true;
// 渲染组件
render(<Addons {...props} />);
// 点击按钮显示所有插件
fireEvent.click(screen.getByRole('button'));
// 断言插件列表已显示
await waitFor(() => {
expect(screen.queryByRole('list')).toBeInTheDocument();
});
});
当所有测试都通过后,交给 QA 进行下一级验证,毕竟人是会犯错的。
与新功能结合的重构
如果时间紧迫,建议先发布新功能,然后单独进行重构。这样需要 QA 重新测试功能的一部分,但这比一次性发布太多代码要好。
判断是否需要重构:
- 如果重构可以避免太多未知风险、降低成本,就不要犹豫!
- 如果只是为了代码美观而重构,谨慎考虑。
- 将业务逻辑从复杂的组件中分离,但不要为了“代码看起来不对”而过度重构。
代码示例
以下例子展示了 Dashboard React 组件渲染一些小部件和一个促销框 (`
// 例子大大简化了
export function Dashboard({ data }) {
// ✅ 重构前的测试
// ❓ 逻辑设置的"条件"在 useUpsellData 中需要
// 其他业务逻辑在这里
// ✅ 新测试
// ❓ 确保现有代码不会中断
// 🧨 高风险 — 可能破坏两个促销,给支持带来沉重负担,错过销售。
// 钩子特定的测试将与
// Dashboard 集成测试重叠。这没问题。
const {
upsell1: { shouldShowUpsell1, upsell1Data },
upsell2: { shouldShowUpsell2, upsell2Data },
} = useUpsellData(conditions);
// ✅ 重构前的测试,特别是如果下面的逻辑
// 与促销混合在一起
// ❓ 确保现有代码不会中断
// 🧨 高风险—可能破坏重要功能,难以追踪。
// 其他业务逻辑在这里
// 这里
// 这里
// ...
// ✅ 现在这个 useEffect 已经没有了,但它的实现
// 应该是 useUpsellData() 👆 测试的一部分
// ❓ 确保现有代码不会中断
// 🧨 高风险—可能破坏第一个促销,导致没有购买。
// useEffect(() => {
// if (condition1 && condition2 && !condition3) {
// setShouldShowUpsell1(true);
//
// loadUpsell1Data().then((bannerData) => {
// setUpsell1Data(bannerData);
// });
// }
// }, [...]);
return (
<div>
<!--
✅ 重构前对小部件的测试
❓ 确保现有代码不会中断
🧨 高|中风险—可能破坏重要的东西。
-->
<Widget1 />
<Widget2 />
...
<!--
✅ 两个幻灯片的新测试
❓ 验证更改
🧨 高|中风险—可能破坏现有功能。
-->
{shouldShowUpsell1 && shouldShowUpsell2 && (
<UpsellSlider>
<UpsellBox1 data={upsell1Data} />
<UpsellBox2 data={upsell2Data} />
</UpsellSlider>
)}
<!--
✅ 重构前这种情况的测试
❓ 确保现有代码不会中断
🧨 高|中风险—可能破坏现有功能。
-->
{shouldShowUpsell1 && <UpsellBox1 data={upsell1Data} />}
<!--
✅ 新测试
❓ 验证更改
🧨 中风险。
-->
{shouldShowUpsell2 && <UpsellBox2 data={upsell2Data} />}
</div>
);
}
这些示例展示了在复杂的组件中添加新功能时,重构可能遇到的问题。主要原因是 <Dashboard /> 的业务逻辑过于复杂,直接处理了多个小部件和部分。
重构,要还是不要?
- 如果代码太复杂,就重构吧,但如果无法验证它的有效性,就不要轻易尝试。
- 如果预见某个部分会发生变化,就伴随新功能一起重构,但如果只是简单的复制粘贴,可以先等等。
- 积极寻找新的方法,确保重构的可预测性,但不要过分依赖 QA,因为人总会犯错。
- 将业务逻辑从复杂的组件中分离,但如果只是为了“代码看起来更整洁”,就不要过度重构。
总而言之,重构需要谨慎,就像下棋一样,步步为营,才能最终取得胜利!
最后,如果你觉得宝哥的分享还算实在,就给我点个赞,关注一波。分享出去,也许你的转发能给别人带来一点启发。