最近让 ChatGPT、Codex 做旧项目重构时,很容易碰到一种特别“顺眼”的建议:
这里有5处重复代码,可以抽成一个公共模块。
乍一看完全没问题。
同样的校验逻辑。
同样的参数转换。
同样的异常处理。
甚至连代码结构都几乎一样。
于是Agent很快帮你抽出一个:
CommonValidator
原来的5份代码变成1份。
Diff一下少了几百行。
测试也全部通过。
看上去这就是一次非常漂亮的重构。
但真正麻烦的事情,往往不是发生在当天。
而是两个月以后。
订单模块说:
这条校验规则要放宽。
支付模块说:
这里必须继续严格。
会员模块又要求:
VIP用户走另一套逻辑。
这时候你才发现:
原来那5段“重复代码”,只是今天长得一样。
它们未来变化的原因完全不同。
而你之前为了消除重复,把5个业务强行绑进了同一个公共模块。
所以这类问题真正要判断的,从来不是:
代码是不是重复。
而是:
这些代码未来是不是会因为同一个原因一起变化。
一、先给核心判断:重复代码,不一定代表同一个抽象
很多人做重构时会天然接受一个原则:
Don't Repeat Yourself。
重复代码不好。
能抽公共逻辑就抽。
但Agent特别擅长发现:
语法相似。
结构相似。
变量名相似。
因为这些特征都非常明显。
问题是:
Syntax Similarity ≠ Semantic Similarity。
两段代码今天可能完全一样。
但它们背后服务的是两个不同业务。
未来变化方向也可能完全不同。
比如订单和退款都需要判断:
amount > 0今天逻辑完全一样。
但半年后:
订单要求:
金额必须大于0。
退款要求:
允许0金额补偿。
营销活动要求:
负数代表冲正。
这时候“同样的判断”已经不再属于同一个业务概念。
所以抽象真正应该看的是:
语义是不是相同。
而不是:
代码长得像不像。
二、一个特别典型的现场:5个Service都有同样的校验
假设Codex扫描仓库以后发现:
OrderService
PaymentService
RefundService
MemberService
CouponService
里都有一段:
检查用户状态。
检查参数。
检查金额。
异常时抛BusinessException。
于是它建议:
统一抽成:
CommonValidator.validate()
代码看起来马上干净很多。
可问题来了。
这5个模块对“合法”的定义真的一样吗?
订单可能允许游客。
支付必须实名认证。
退款可能允许冻结账号处理历史订单。
会员模块又有自己的等级逻辑。
今天它们代码一样,
可能只是因为:
当前业务规则碰巧重合。
不是因为:
它们本来就是同一个Domain Rule。
这就是最容易误判的地方。
三、Agent为什么特别容易“过早抽象”?
因为从代码表面看,重复是非常明显的Signal。
比如:
5个文件里都有15行相似逻辑。
这对Agent来说很容易识别。
而“未来是不是会分化”,却没有那么容易从当前代码直接看到。
这需要结合:
业务边界。
模块职责。
历史变更。
产品规则。
团队所有权。
未来演进方向。
所以如果你只给Codex一个任务:
帮我重构重复代码。
它很容易优化:
当前代码形态。
却没有足够Evidence判断:
未来变化方向。
这也是为什么Agent参与重构以后,人更应该补上:
业务演进判断。
四、真正应该问的是:它们为什么会一起变化?
这是判断能不能抽公共模块最重要的问题。
假设5段代码都在做:
手机号格式校验。
如果未来规则变化时:
所有模块都必须同步变化。
比如统一从11位手机号,升级到支持国际号码。
那它们大概率确实属于:
同一个稳定抽象。
这时候抽成:
PhoneNumberValidator
就很合理。
但如果5段代码虽然都在判断:
用户状态。
可订单、支付、退款对用户状态的要求各不相同,
那么它们今天相似,
不代表应该共享。
一个非常实用的问题是:
未来如果这里发生变化,谁会要求它变化?
如果答案是:
同一个业务规则。
可以考虑抽象。
如果答案是:
5个不同业务团队。
那就要谨慎。
五、错误抽象最麻烦的地方:它制造Change Coupling
本来5个模块互相独立。
订单改订单。
退款改退款。
支付改支付。
抽成公共模块以后,情况变了。
支付要改一条规则。
你修改:
CommonValidator
结果马上要问:
订单会不会受影响?
退款呢?
会员呢?
优惠券呢?
于是一次原本只属于支付的需求,
变成:
5个模块一起回归。
这就是:
Change Coupling——变更耦合。
代码确实少了。
但变更影响面变大了。
真正危险的是:
公共模块越来越多人依赖以后,
任何人都不敢轻易改。
最后它会慢慢变成:
一个看起来“公共”,实际上谁都害怕碰的模块。
六、所以“减少代码行数”不是抽象成功的指标
Agent重构以后经常会给出非常漂亮的结果:
删除420行重复代码。
新增80行公共实现。
测试全部通过。
从Diff来看:
非常成功。
但工程上真正应该继续看:
新增一个业务规则时,要改几个地方?
公共模块改变时,需要回归多少下游?
一个团队的需求,会不会强迫其他模块同步升级?
如果这些成本上升了,
那么:
代码重复减少,并不代表系统复杂度下降。
你只是把:
重复成本
换成了:
耦合成本。
而后者往往更难看见。
七、什么时候应该暂时保留重复代码?
这句话很多人不太喜欢:
有时候重复代码是可以接受的。
但实际工程里确实如此。
尤其当:
业务刚开始发展。
规则还不稳定。
多个模块只是暂时长得一样。
未来方向还看不清。
这时候保留两三份简单实现,有时比过早抽公共模块更安全。
因为重复的成本是:
修改时多改几处。
而错误抽象的成本可能是:
以后每次改动都要担心所有下游。
如果现在还不知道两段代码是否属于同一个长期概念,
我更倾向于:
Wait for the Pattern to Stabilize。
先观察变化。
不要看到两次重复就马上抽。
八、我更关注“第二次变化”而不是“第一次重复”
假设两个模块第一次都需要:
检查用户等级。
这时候代码一样。
还不能证明应该抽。
后来规则变化了一次。
如果两个模块仍然同时变化成同样逻辑,
说明它们背后的概念可能确实一致。
再过一段时间第二次变化。
仍然一起变化。
这时候再抽象,可信度就高很多。
也就是说:
Stable Abstraction往往来自多次共同变化。
不是来自第一次代码重复。
这点特别适合Agent重构。
因为Agent很容易在第一次看到重复时就出手。
而人可以提醒它:
先查这几段代码过去半年是不是经常一起变化。
历史变更其实是一种很重要的Evidence。
九、Git历史比当前代码更能判断“是不是同一个概念”
如果我怀疑5段重复代码是不是值得抽,
我会先看:
过去几次修改。
比如:
订单模块修改了4次。
退款模块修改了3次。
但从来没有一起变化。
那说明:
虽然今天代码相似,
它们的演化路径其实是独立的。
反过来,如果过去一年:
这5处每次规则变化都一起改,
而且原因也相同,
那公共抽象就更有价值。
所以让ChatGPT、Codex重构之前,可以先让它做一件事:
分析Co-change History。
不是只扫描当前代码。
十、公共模块还有一个问题:到底谁负责?
假设最终真的抽出了:
CommonValidator
接下来马上会出现一个很现实的问题:
谁拥有它?
订单团队?
支付团队?
基础架构团队?
还是谁都能改?
如果Owner不清楚,
公共模块很容易慢慢变成:
谁有需求谁就往里面加参数。
最后:
validate(user, scene, source, mode, strict, ignoreX...)越来越复杂。
因为每个业务都想让公共模块照顾自己的例外。
于是原来为了“消除重复”建立的抽象,
最后变成:
所有业务差异的集中地。
这种公共模块,比5份重复代码更难维护。
十一、一个好抽象应该让接口越来越稳定
真正成熟的公共模块通常有一个特点:
使用方很多。
但它本身变化频率越来越低。
比如:
日期解析。
统一ID格式。
手机号格式。
标准加密。
协议序列化。
这些概念边界相对稳定。
相反,如果一个公共模块:
每来一个业务需求就要加一个if。
每接一个新模块就多一个参数。
每次修改都要通知很多下游。
那很可能说明:
抽象层级选错了。
它不是一个真正稳定的公共概念。
只是把多份业务代码强行搬到一个文件里。
十二、我更建议让Agent先做“重复分类”,而不是直接抽象
以后看到Codex提示:
发现多处重复代码,建议提取公共方法。
不要马上Accept。
可以先让它回答三个问题:
这些代码只是语法相似,还是业务语义相同?
它们过去是否经常一起变化?
未来一个模块单独变化时,会不会被公共抽象限制?
然后把重复分成:
Mechanical Duplication
例如格式转换、通用解析。
通常更适合抽。
Domain Duplication
不同业务暂时有同样规则。
谨慎。
Accidental Similarity
只是碰巧长得一样。
通常不应该抽。
这一步比直接“Extract Method”有价值得多。
十三、如果已经抽错了,怎么判断?
一个很明显的信号是:
公共模块开始出现越来越多:
if (scene == ...)
if (type == ...)
if (source == ...)
甚至每个业务都要传:
一个特殊Flag。
这说明:
原来被认为相同的逻辑,正在不断分化。
另一个信号是:
每次只改一个业务需求,
却必须让5个下游一起跑完整回归。
这时候就要重新问:
这个公共模块是不是已经制造了更多耦合?
有时候正确动作不是继续优化它。
而是:
重新拆开。
十四、一个简单指标:Change Coupling Rate
这篇我建议只留一个指标:
Change Coupling Rate——变更耦合率
可以简单理解为:
某个公共模块发生一次业务修改后,需要一起验证或修改的下游模块数量 ÷ 总下游数量
假设一个公共Validator有5个业务依赖。
支付规则变化一次,
结果5个模块都必须重新验证。
说明耦合很高。
如果公共模块长期稳定,
只有真正的公共规则变化时才影响全部下游,
那这个抽象就比较健康。
所以真正值得看的不是:
少了多少重复代码。
而是:
一个业务变化会被放大到多少地方。
十五、什么时候我会明确支持抽公共模块?
如果满足下面几个条件,我通常会更放心:
业务语义明确相同。
过去经常一起变化。
未来变化原因大概率一致。
接口边界清晰。
Owner明确。
可以独立测试。
下游不需要不断传特殊参数。
这时候抽象真正减少的是:
重复决策。
而不仅仅是:
重复代码。
这才是公共模块最有价值的地方。
十六、Plus和Pro怎么判断?
如果你平时主要让ChatGPT、Codex:
处理单模块重构。
分析几处重复代码。
查看少量Git历史。
判断一次公共方法是否值得提取。
这类任务比较集中,Plus通常够用。
关键是不要只让Agent看当前Diff。
最好把:
模块职责。
历史修改。
未来需求。
一起给它。
如果你的日常已经是:
大型Monorepo。
几十个模块。
大量Shared Library。
一次重构需要同时分析代码依赖、Git历史、下游调用、测试和兼容范围,
并让Codex连续做影响分析、重构、回归和二次调整,
这种长时间、多模块的架构工作已经成为常态,
那Pro会更适合。
真正的判断标准仍然是:
ChatGPT、Codex是不是已经长期参与大型工程决策,而不只是写几段代码。
最后
Agent发现5处重复代码,为什么不能马上抽成一个公共模块?
因为:
重复代码不等于重复业务概念。
今天长得一样的两段代码,
未来可能因为完全不同的业务原因分别变化。
如果过早抽象,
你消除的是:
几百行重复。
但可能引入的是:
长期Change Coupling。
所以真正值得判断的不是:
这5段代码有多像?
而是:
它们以后会不会因为同一个原因一起变化?
如果只是当前实现相似,
先保留重复并不可怕。
等变化模式真正稳定以后再抽,
往往更安全。
Agent时代真正需要防的,也不是代码重复本身。
而是:
AI太快地帮我们把“暂时相似”,固化成了“长期耦合”。
持续分享 Codex、大模型开发与 AI 编程实战内容
也整理了稳定的 Plus/Pro 订阅渠道,有需要下方可自取。