AI 时代的《重构》

前言

在日常开发中,经常会遇到这样的场景:系统新功能工期紧张,但是与旧代码的设计逻辑差距很大,是选择用几天时间去做与新功能毫不相干的重构设计,还是在继续沿用旧代码的逻辑?的确,新的设计更加纯粹、整洁,但是要交付的是可以有效运行的代码,不是用来取悦我们自己的看不见的设计,这时候应该如何抉择?

AI 辅助编程普及之后,这个问题有了新的背景。一方面,生成代码的成本大幅降低,代码堆积的速度比以前更快;AI 倾向于就地添加代码,很少从整体设计上考虑安放的位置,如果没有重构的习惯,项目变成遗留系统的速度只会更快。另一方面,提炼函数、改名、搬移代码这类机械性的重构操作,AI 已经可以快速完成,"工期紧张,没时间重构"的理由比以前更难成立。同时,AI 生成的代码看起来总是对的,但看起来对和真的对是两回事,这让作为重构安全保障的测试变得更加重要。

这篇文章将结合《重构》中的例子和思想,说明重构在开发中的作用,以及它在 AI 时代的新意义。

重构的目的

以下摘自《重构》:

在软件开发的大部分历史时期,大部分人相信应该先设计而后编码:首先得有一个良好的设计,然后才能开始编码。但是,随着时间流逝,人们不断修改代码,于是根据原先设计所得的系统,整体结构逐渐衰弱。代码质量慢慢沉沦,编码工作从严谨的工程堕落为胡砍乱劈的随性行为。"重构"正好与此相反。哪怕手上有一个糟糕的设计,甚至是一堆混乱的代码,我们也可以借由重构将它加工成设计良好的代码。重构的每个步骤都很简单,甚至显得有些过于简单:只需要把某个字段从一个类移到另一个类,把某些代码从一个函数拉出来构成另一个函数,或是在继承体系中把某些代码推上推下就行了。但是,聚沙成塔,这些小小的修改累积起来就可以根本改善设计质量。这和一般常见的"软件会慢慢腐烂"的观点恰恰相反。

保持重构的习惯,可以最大程度减少出现问题的概率,减少团队之间的配合、沟通成本,可以让一个程序在开发过程中持续保有良好的设计。

重构的原则

1. 何谓重构

重构是在不改变代码外在行为的前提下,调整内部结构的过程。其核心目标为:

  • 提升可读性
  • 降低维护成本
  • 增强扩展性

并且要保持 YAGNI(You Ain't Gonna Need It) 原则,YAGNI 并不是"不做架构性思考"的意思,是要避免过度设计,仅在必要时重构。

2. 为何重构

  • 预防性维护:减少未来修改的风险。
  • 提升开发速度:清晰的代码结构减少调试时间。
  • 促进代码复用:模块化设计更易复用。

3. 构筑测试体系

无论每次重构多么简单,养成重构后即运行测试的习惯非常重要。做完一次修改就运行测试,这样即使犯错,只需要考虑一个很小的改动范围,这使得查错与修复问题易如反掌。这就是重构过程的精髓,小步修改,以及它带来的频繁反馈,正是防止混乱的关键。

4. 重构与性能

不赞成为了提高设计的纯洁性而忽视性能,把希望寄托于更快的硬件身上也绝非正道。已经有很多软件因为速度太慢而被用户拒绝,日益提高的机器速度也只不过略微放宽了速度方面的限制而已。但是,换个角度说,虽然重构可能使软件运行更慢,但它也使软件的性能优化更容易。除了对性能有严格要求的实时系统,其他任何情况下"编写快速软件"的秘密就是:先写出可调优的软件,然后调优它以求获得足够的速度。

重构的方法

1. 提炼/内联函数

介绍:提炼函数和内联函数是两个相反的重构方法,提炼函数是一个常见的可自动完成的重构。如果我是用 Java 编程,我会本能地使用 IDE 的快捷键来完成这项重构。在我撰写本书时,JavaScript 工具对此重构的支持仍不是很健壮,因此我必须手动重构。这不是很难,当然我还是需要小心处理那些局部作用域的变量。

JavaScript 示例:

scss 复制代码
// 坏味道:函数过长
function processOrder(order) {
  // 验证订单...
  // 计算价格...
  // 生成发票...
  // 发送邮件...
}
​
// 重构后
function processOrder(order) {
  validateOrder(order);
  const total = calculateTotal(order);
  generateInvoice(order, total);
  sendConfirmationEmail(order);
}

2. 提炼/内联变量

介绍:提炼变量是把复杂表达式拆成有名字的变量,用名字解释表达式的含义;内联变量则相反,当变量名并不比表达式本身更清楚时,就应该去掉这层多余的间接。两者方向相反,目的相同:让读代码的人少费力气。

JavaScript 示例:

scss 复制代码
// 坏味道:表达式含义需要读者自行解析
if (order.amount > 100 && order.isVip && !order.hasOverdue) {
  // 发放优惠券...
}
​
// 重构后:提炼变量,名字解释条件
const isLargeOrder = order.amount > 100;
const hasGoodCredit = order.isVip && !order.hasOverdue;
​
if (isLargeOrder && hasGoodCredit) {
  // 发放优惠券...
}
kotlin 复制代码
// 坏味道:变量名不比表达式更清楚,反而多了一跳
const basePrice = order.basePrice;
return basePrice * 0.9;
​
// 重构后:内联
return order.basePrice * 0.9;

3. 函数改名

介绍:函数的名字应该说明它做什么。当需要一段注释来解释某个函数时,往往说明名字起得不够好。改名是成本最低的重构,现代编辑器都能安全地完成批量重命名,唯一要做的,是想出一个好名字。

JavaScript 示例:

javascript 复制代码
// 坏味道:名字模糊,必须阅读实现才能理解
function calc(order) {
  return order.items.reduce((sum, i) => sum + i.price * i.count, 0);
}
​
// 重构后:名字即文档
function calculateOrderTotal(order) {
  return order.items.reduce((sum, i) => sum + i.price * i.count, 0);
}

4. 变量改名

介绍:变量名决定了读者理解代码的成本。作用范围小、生命周期短的变量,名字可以从简;跨越多个函数、被多处引用的变量,必须认真命名。名字含糊的变量积累多了,读代码就变成了逐个猜测的过程。

JavaScript 示例:

ini 复制代码
// 坏味道:缩写和临时名
const tmp = users.filter(u => u.age >= 18);
const res = tmp.map(u => u.name);
​
// 重构后
const adultUsers = users.filter(u => u.age >= 18);
const adultUserNames = adultUsers.map(u => u.name);

5. 封装

场景 :直接暴露变量导致数据被意外修改。 JavaScript 示例:

ini 复制代码
// 重构前
let defaultConfig = { timeout: 1000 };
​
// 重构后
const config = {
  getDefault() { return { ...defaultConfig }; },
  setTimeout(value) { defaultConfig.timeout = value; }
};

6. 拆分

场景 :一个函数处理多个逻辑阶段。 重构手段:拆分为独立的函数,每个阶段单独命名、单独测试,阶段之间用结构清晰的数据传递。

kotlin 复制代码
// 重构前:混合计算与格式化
function generateReport(data) {
  const processedData = data.map(processItem);
  return processedData.join('\n');
}
​
// 重构后:明确分离阶段
function processData(data) {
  return data.map(processItem);
}
​
function formatReport(processedData) {
  return processedData.join('\n');
}

7. 替换算法

介绍:当发现某个函数的实现可以用更简单、更清晰的方式表达时,就把函数体整个替换掉。这不是微调,而是整体置换,前提是已经充分理解这段逻辑,并且有测试保障。

JavaScript 示例:

bash 复制代码
// 重构前:手写查找逻辑
function findUser(users, id) {
  for (let i = 0; i < users.length; i++) {
    if (users[i].id === id) {
      return users[i];
    }
  }
  return null;
}
​
// 重构后:使用语言内置能力
function findUser(users, id) {
  return users.find(u => u.id === id) ?? null;
}

8. 搬移特性

介绍:判断一个函数是否放错了位置,看它操作的数据在哪里。如果函数总是访问另一个对象的字段,它多半应该搬到那个对象里去。数据和行为放在一起,修改时就不需要两头跑。

JavaScript 示例:

javascript 复制代码
// 重构前:行为和数据分离
class Order {
  constructor(items) {
    this.items = items;
  }
}

function totalWeight(order) {
  return order.items.reduce((sum, i) => sum + i.weight, 0);
}

// 重构后:把行为搬到数据所在的地方
class Order {
  constructor(items) {
    this.items = items;
  }

  totalWeight() {
    return this.items.reduce((sum, i) => sum + i.weight, 0);
  }
}

9. API 重构

场景 :模块边界处的函数签名混乱,参数列表过长,调用方需要了解太多内部细节才能正确使用。 重构手段:收敛接口,比如用参数对象替代长参数列表,把查询和修改状态的逻辑分开。

JavaScript 示例:

php 复制代码
// 坏味道:参数列表过长,调用处难以阅读
function createUser(name, email, age, isAdmin, needNotify, region) {
  // ...
}

createUser('张三', 'zhang@example.com', 30, false, true, '华东');

// 重构后:参数对象化,调用处自解释
function createUser({ name, email, age, isAdmin = false, needNotify = true, region }) {
  // ...
}

createUser({
  name: '张三',
  email: 'zhang@example.com',
  age: 30,
  needNotify: true,
  region: '华东',
});

重构与 vibecoding

vibecoding 是 AI 辅助编程的典型实践:开发者用自然语言描述需求,AI 生成代码,人负责审查和调整。这种模式下,代码的生产速度前所未有地快,但问题也同样明显------每个功能独立看起来都对,放在一起却可能结构混乱;生成的命名往往随意,temp、data、result 这类万能词到处都是;功能越堆越多,模块之间的边界却越来越模糊。如果不对生成结果加以约束,项目变成遗留系统的速度会比手写时代更快。

1. 生成-重构循环

vibecoding 最容易陷入的误区是"生成-生成-再生成":一个功能刚跑通,马上让 AI 写下一个。这种节奏下,代码以最快的速度腐坏。

正确的做法是"生成-重构-再生成":每完成一个功能,先让 AI 把刚写的代码重构一遍,确认结构清晰、命名合理、测试通过,再进入下一个功能。重构不是开发完成后的补救,而是每一次生成交互中的自然环节。

2. 用重构语言引导 AI

不要跟 AI 说"让代码更好"或"优化一下",这类指令太模糊,AI 往往只会做一些表面调整。有效的做法是指明具体的重构方法:

  • "把这个函数超过 20 行的部分提炼成独立函数"
  • "把这三个 if-else 改成策略模式"
  • "把这个变量名改成能说明用途的名字"

AI 擅长执行具体的重构指令,不擅长理解模糊的"优化"。给出明确的重构手段,AI 的执行质量会高得多。

3. 人做架构判断,AI 做执行

在 vibecoding 中,人的角色从"编写者"变成了"架构师+审查者"。

重构什么、何时重构、边界划在哪里,这些决策由人来下。AI 可以执行"提炼函数",但该不该提炼、提炼到什么程度,取决于人对业务和系统的理解。怎么提炼、怎么搬移、怎么改名,这些执行交给 AI 完成。

这种分工下,人的价值不在于打字速度,而在于判断代码该往哪里走。

4. 测试是 vibecoding 的重构保障

AI 生成代码后,第一件事不是让它继续写新功能,而是让它补测试。

有了测试,后续的重构才有底气。vibecoding 的节奏很快,没有测试的快节奏是危险的。让 AI 生成测试、运行测试、再重构,这个顺序不能颠倒。

总结

《重构》通过详实的案例与系统的方法论,揭示了重构的核心逻辑:

  1. 识别坏味道:如重复代码、过长函数等。
  2. 小步修改:每次重构仅解决一个问题。
  3. 测试保障:自动化测试是重构的基石。

在 JavaScript 中,结合函数式编程(如管道操作、纯函数)与面向对象特性(多态、委托),可高效实现重构目标。

回到前言的问题:工期紧张时还要不要重构?在 AI 时代,执行重构的成本显著降低,小步重构可以随时进行;在 vibecoding 中,重构不再是开发完成后的补救措施,而是每一次生成交互中的自然环节。但涉及整体设计调整的重构依然需要慎重,因为设计决策仍然要由人来做。AI 可以执行"提炼函数",但该不该提炼、边界划在哪里,取决于人对业务和系统的理解。重构什么、何时重构、重构成什么样子,这些判断无法外包。从这个角度看,这本书的价值没有降低,反而更值得重读。

相关推荐
诚心呈意共享外卖配送系统2 小时前
即时零售配送系统怎么选?一份可落地的技术选型评估清单
架构·系统架构·创业创新·零售
瓶瓶持续学习中2 小时前
数字孪生与空间智能技术架构解析:五类厂商路线及选型方法
大数据·网络·架构
JPower_mr.g3 小时前
SmartCall v1.0.9 发布:智能体知识库上线,让 AI 客服先查资料再回答
java·ide·人工智能·macos·架构·开源·xcode
天远数科3 小时前
零信任架构实战:基于天远企业年报信息核验构建自动化供应商准入资质合规审查网关
运维·人工智能·架构·自动化
真上帝的左手5 小时前
10. 软件设计&架构-架构师之路-1.究竟什么是架构设计?
架构·架构设计
摸鱼仙人~5 小时前
校招帮简历字段匹配架构分析报告
java·前端·架构
2603_954708315 小时前
微能网协调控制箱的架构优势对能源管理的影响?
大数据·人工智能·分布式·安全·架构
zhongben123_6 小时前
从0到1开发一款生鲜配送APP:核心技术难点与架构避坑指南
大数据·运维·人工智能·ios·架构·web app
Thneonl6 小时前
0.18 也被 DENY:治理必须是台确定性机器
人工智能·架构