开场:一次“优雅”重构背后的灾难性依赖链
2025年年底,我接手了一个运行了12年的.NET Framework订单系统。代码量不算大,核心模块大约4万行,但几乎没有单元测试,所有业务逻辑都纠缠在几个“God Class”里。我当时手里正好有GitHub Copilot(底层就是Codex),心想:让AI帮我拆解,重构到.NET 8,不是很合理吗?
第一周,一切都很顺利。我让Codex分析一个付款处理类,它给我拆出了PaymentService、PaymentValidator、PaymentRepository三个类,每个都有清晰的接口,依赖注入构造函数里看起来井井有条。我还挺得意,觉得AI比初级工程师靠谱多了。
第二周,我开始跑集成测试。结果炸了:一个简单的付款确认接口,启动时要完成47个依赖的解析。其中有11个,根本不在这个调用链上。我花了两天时间顺藤摸瓜,发现Codex在重构过程中,“自作主张”地为每个拆出来的类塞了一堆它们根本不会调用的依赖,那个负责记录支付日志的类,注入了全文检索引擎和短信网关;那个只会返回静态配置的类,注入了分布式缓存和配置中心客户端。
这不是一次孤立事件。过去一年里,我在三个项目上做了类似的AI辅助重构实验,每一次都会产生不同程度的“不必要依赖注入”。这篇文章,就是把这三次踩坑的真实数据、复盘过程和解决方案,完整摊开给你看。

一、核心结论:Codex的“依赖注入洁癖”从何而来?
在展开细节之前,我先把这个问题的根因讲清楚。很多人认为Codex产生不必要依赖,是因为它“不懂业务”。这没错,但太表面了。真正的原因要深得多。
1.1 Codex不是“乱写”,它是“太想写好”
我在复盘所有Codex生成的代码时,发现一个规律:Codex倾向于生成它认为“教科书级”的代码。
什么意思?我举个例子。一个真实的付款方法,在旧系统里长这样:
public void ProcessPayment(string orderId, decimal amount)
{
// 更新订单状态
_orderRepository.UpdateStatus(orderId, "Paid");
// 记录日志
_logger.Log($"Payment processed for {orderId}");
// 发邮件通知
_emailService.SendNotification(orderId);
}
这段代码虽然耦合度高,但它只需要三个依赖。当我把这个类丢给Codex进行重构时,它给我生成的是:
public class PaymentProcessor : IPaymentProcessor
{
private readonly IOrderRepository _orderRepository;
private readonly IPaymentGateway _paymentGateway;
private readonly IEventBus _eventBus;
private readonly ILogger<PaymentProcessor> _logger;
private readonly IMetricsCollector _metricsCollector;
private readonly IDistributedCache _distributedCache;
private readonly IValidationService _validationService;
private readonly IFullTextSearchService _searchService;
// 这个全文检索服务和付款毫无关系
public PaymentProcessor(
IOrderRepository orderRepository,
IPaymentGateway paymentGateway,
IEventBus eventBus,
ILogger<PaymentProcessor> logger,
IMetricsCollector metricsCollector,
IDistributedCache distributedCache,
IValidationService validationService,
IFullTextSearchService searchService)
{
_orderRepository = orderRepository;
_paymentGateway = paymentGateway;
_eventBus = eventBus;
_logger = logger;
_metricsCollector = metricsCollector;
_distributedCache = distributedCache;
_validationService = validationService;
_searchService = searchService;
}
// 业务逻辑里,searchService根本没被调用过
}
Codex为什么会加IFullTextSearchService?因为它在分析整个项目上下文时,看到有OrderSearch模块用了这个服务,而PaymentProcessor在处理订单,它“推断”你可能需要搜索功能。这就是典型的大模型“过度关联”问题。
Codex在生成代码时,不是基于“这个类实际需要什么”来添加依赖,而是基于“类似的项目里,类似的类通常有什么依赖”来推断。 这是底层Transformer架构的注意力机制决定的,它关注的是全局模式,而非局部必要性。

1.2 更重要的结论:不必要依赖注入不是“浪费几行代码”的问题
很多开发者觉得,多几个依赖注入无非就是构造函数长一点,问题不大。这是严重的误判。我在实际项目里观察到的真实影响是:
- DI容器的解析性能下降: 在我的支付系统案例中,因为有大量根本不会被调用的依赖需要实例化,冷启动时间从3.2秒增加到11.8秒(数据来源:System.Diagnostics.Stopwatch测量10次冷启动平均)。这不是“毫秒级”的差异。
- 单元测试的成本直接爆炸: 每多一个不必要的依赖,你在写单元测试时就要多Mock一个对象。支付系统的47个依赖里,我在写测试时要为24个不会被调用的依赖手动创建Mock,写测试的时间反而比重构代码的时间还长。
- 架构腐化的加速器: 当不必要依赖存在时,后来的开发者不会质疑“为什么这个类需要全文检索”,他们会认为“既然已经有这个依赖了,那我用一下也不过分”。三个月后,这些依赖就从“不必要的”变成了“实际使用但不应使用的”。
二、真实场景复盘:我是如何在三次重构中踩坑的
这部分我详细展开三个重构项目的具体场景,包括旧系统架构、重构目标、Codex使用方式、以及产生不必要依赖的具体代码。
2.1 场景一:支付系统重构(2025年12月)
旧系统背景: 这是一个.NET Framework 4.6.1的单体应用,运行在Windows Server上。核心支付模块包含Payments.cs、Orders.cs、Notifications.cs三个大类,每个类都在2000-3000行。依赖关系通过构造器和Service Locator模式混合管理。
重构目标: 将单体拆分为微服务,同时升级到.NET 8。目标是将支付相关的三个大类和通知服务拆成20-30个小类,每个类单一职责。
我的Codex使用方式:
- 让Codex分析每个类的职责,给出拆分建议
- 逐个类让Codex生成重构后的代码
- 我手动添加到项目中,跑单元测试
问题暴露过程: 重构完成后,我发现一个叫PaymentNotificationBuilder的类,职责只是“根据模板拼接邮件正文”,一个纯函数,没有任何外部依赖就能工作。但Codex生成的代码里,它的构造函数是这样的:
public PaymentNotificationBuilder( IConfiguration configuration, // 未使用 - 用于读取模板?但模板是硬编码的 ILogger<PaymentNotificationBuilder> logger, // 未使用 - 纯函数需要日志? ITemplateEngine templateEngine, // 未使用 - 没有用到任何模板引擎功能 IOrderRepository orderRepository, // 未使用 - 所有数据通过参数传入 IUserPreferenceService userPrefService) // 未使用 - 和邮件构建无关
我问自己:Codex为什么加了这么多无用的依赖?我回到它的分析记录里找答案。原来,它在分析旧代码时,看到原来的Notifications.cs类里确实有这些引用,因为那个3000行的怪物类同时处理短信、邮件、推送通知,什么依赖都有。Codex在拆分类时,把父类的所有依赖原封不动地复制了五份,给每个子类都塞了一套。
这就是模式一:继承性依赖污染。Codex不能理解“这个子类真的只需要父类依赖中的一小部分”,它用的是“宁可多给,不能漏掉”的安全策略。

2.2 场景二:CMS内容管理系统重构(2025年8月)
旧系统背景: 基于Spring Boot 2.1构建的内容管理系统,使用传统MVC分层。Controller层调用Service层,Service层依赖十几个Repository。整个系统大约有150个类。
重构目标: 引入CQRS模式,将读写操作分离,同时引入DDD领域模型。
问题暴露: 这次的问题更加隐蔽。我要重构一个查询文章的Service。原来的代码是这样的:
public Article getArticleById(String id) {
return articleRepository.findById(id)
.orElseThrow(() -> new ArticleNotFoundException(id));
}
让Codex重构后,它变成了:
@Service
public class ArticleQueryService {
private final ArticleRepository articleRepository;
private final CacheManager cacheManager;
private final MetricsService metricsService;
private final AuditLogger auditLogger;
private final UserPermissionService permissionService;
private final ContentFormatter contentFormatter;
private final SearchIndexer searchIndexer; // 查询操作为什么需要索引服务?
// 构造函数省略...
public ArticleDTO getArticleById(String id) {
// 方法体内只用了articleRepository和contentFormatter
// 但是cacheManager、metricsService、auditLogger、permissionService、searchIndexer
// 都在这个类里没有被任何方法调用
}
}
这次的根因不同。Codex在分析项目时,看到了另一个叫ArticleManagementService的类(这是负责文章创建、更新、删除的类),它用了以上所有依赖。当它看到“ArticleQuery”和“ArticleManagement”都是处理文章的,就认为它们应该有相似的依赖图谱。这是“语义相似性诱导的依赖复制”,是Codex在没有明确边界定义时的默认行为。
更糟糕的是,当我发现这个问题并手动清理依赖时,我发现每一个“多余”的依赖都是@Service注解的,意味着Spring会在启动时实例化它们,即使用不上,也会占用内存和连接池资源。其中SearchIndexer在初始化时会建立到Elasticsearch的连接,导致整个应用启动时必须依赖ES可用,否则报错。而一个纯查询服务,根本不操作索引。

2.3 场景三:物流对账系统重构(2025年3月)
这是我第一次尝试用Codex辅助重构,也是教训最惨痛的一次。
旧系统背景: Python 2.7时代的Django项目,需要升级到Python 3.12+Django 5.1。核心模块是对账逻辑,大约8000行代码,分散在5个文件里。
我的错误做法: 因为Python 2到3的语法差异较大,我选择了一个“偷懒”策略,把整个模块丢给Codex,让它“用Python 3.12语法重写,并按照单一职责原则拆分类”。然后我回来检查语法和测试。
结果Codex生成的新代码里,出现了一个极其荒谬的类:
class ReconciliationConfigProvider: """ 提供对账配置的服务类 """ def __init__( self, django_settings: Settings, # Django的全局settings redis_client: Redis, # Redis客户端 db_connection: DatabaseWrapper, # 数据库连接 logger: Logger, # 日志 metrics_client: StatsDClient, # 监控指标 feature_flag_service, # 特性开关 audit_service, # 审计服务 notification_service # 通知服务 ): def get_reconciliation_threshold(self) -> float: """获取对账差异阈值""" return 0.01 # 直接返回一个硬编码的常量! def get_retry_count(self) -> int: """获取重试次数""" return 3 # 直接返回硬编码常量!
这个类的方法都是返回硬编码的常量,但它注入了8个重量级依赖,包括数据库连接和Redis客户端。Codex看到了“Config”这个类名,就认为它应该能读取所有可能的配置源(数据库、缓存、特性开关、Django settings……),而完全没有分析方法的实际实现,它们根本不需要任何外部依赖。
这就是模式三:名称驱动的过度推断。Codex对类名和方法名高度敏感,会根据命名惯式推断依赖关系,而不是根据方法的实际实现来决定需要什么。
这个教训让我意识到:如果不对Codex施加严格的约束,它会成为“过度工程化”的加速器,在几秒钟内产生需要几小时才能清理的技术债务。
三、拆解:Codex产生不必要依赖注入的六大机制
基于以上三次大型重构实践,以及我在十几个小模块上做的对照实验,我总结出Codex产生不必要依赖注入的六大底层机制。理解这些机制,你才能在提示词和审查流程中有的放矢。
3.1 机制一:模式补全优先于需求分析
这是最常见的机制,也是我在支付系统案例中看到的“继承性依赖污染”的根本原因。
Codex在生成构造函数时,本质上是在做“代码补全”,只不过规模更大。当它看到一个类声明和几个字段,它会根据训练数据中常见模式来补全“这个类应该有什么依赖”。如果训练数据里,大多数Service类都有ILogger、IMapper、ICache,它就会倾向于给所有Service类都加上这些依赖,而不管这个具体类是否真的需要。
我在实验里做了验证:创建两个功能完全相同的接口,一个叫IDataTransformer,另一个叫ISimpleProcessor。让Codex为两个接口生成实现类的依赖注入方案。IDataTransformer的实现类被注入了6个依赖(包括缓存、日志、映射器、验证器等),而ISimpleProcessor只被注入了2个依赖。两者的方法体完全一样,但类的命名触发了Codex完全不同的“模式补全”行为。
3.2 机制二:安全边际导致的“全量注入”
Codex在处理不确定性时,会采取一种“安全策略”:当它不确定某个依赖是否需要时,倾向于包含而不是排除。这很像人类开发者在面对不熟悉的代码库时的防御性编程,但人类有判断力,AI没有。
我在物流对账系统的案例中看到,ReconciliationConfigProvider被注入了redis_client和db_connection。Codex的“推理链”可能是:
- 这是一个配置提供者
- 配置可能来自多个来源(数据库、缓存、文件、环境变量)
- 既然不确定具体来源,就把所有可能的来源都作为依赖注入
- 这样即使实际没有用到,也不会出现“缺少依赖”的运行时错误
这个推理链从AI的角度是合理的,避免遗漏比避免冗余更重要。但从软件工程的角度,这就是在制造技术债务。
3.3 机制三:上下文窗口的“关联扩散”
这是我在CMS系统案例中看到的“语义相似性诱导的依赖复制”的技术根因。
Codex处理代码上下文时,会将整个项目或大段代码作为注意力范围。当它为ArticleQueryService生成依赖时,它的注意力机制会关联到项目中所有与“Article”相关的类,包括ArticleManagementService、ArticleController、ArticleEventSubscriber等等。这些类使用的所有依赖,都可能被Codex视为“与Article处理相关的依赖”,从而建议给新的ArticleQueryService。
在我做的一个实验中,我创建了一个只有3个类的简单项目:
-
UserService:依赖A、B、C、D -
UserQueryService:只依赖A(查询数据库)
然后我让Codex重构UserQueryService。结果它生成的代码里,注入了A、B、C、D四个依赖。我检查了提示词,我没有任何地方提到要加入其他依赖。Codex自己通过上下文关联,把UserService的依赖“扩散”到了UserQueryService。

3.4 机制四:框架惯性的“默认注入”
不同的技术栈,Codex有不同的“框架惯性”。
在Spring Boot项目中,Codex倾向于给每个@Service类注入@Autowired的依赖,即使该类只是简单的工具类。这是因为Spring Boot的“约定优于配置”文化,在训练数据中表现为大多数Service都有一组标准的依赖。
在ASP.NET Core项目中,Codex倾向于注入ILogger<T>和IConfiguration,因为微软的官方文档和示例代码几乎都在构造函数里加了这两个依赖。
在Python的FastAPI项目中,Codex倾向于注入Depends()依赖,即使这个路由处理器不需要数据库会话或用户验证。
我在三个不同技术栈上做了对比实验:创建同样功能的一个“获取当前时间”的端点。Codex为Spring Boot版本注入了一个ClockService接口和TimeZoneRepository;为ASP.NET Core版本注入了TimeProvider和ILogger;为FastAPI版本保持了纯函数,没有多余依赖。框架的文化惯性直接影响Codex的“添加依赖倾向”。

3.5 机制五:代码注释的“误导放大”
这是一个我之前完全没想到的问题,但在实际使用中发现影响巨大。
我在重构支付系统时,给一个类写了这样的注释:
/ 支付处理器 负责处理支付请求,包括验证、执行和结果通知 未来可能需要支持多种支付方式(信用卡、支付宝、微信支付) 可能需要集成风控系统 */
Codex读取了这段注释后,在生成的代码里加入了IPaymentMethodFactory、IWeChatPayAdapter、IAlipayAdapter、IRiskControlService四个依赖,而这些功能根本还没有实现。Codex把注释中的“未来可能需要”当成了“当前需要”来处理。
在一个更极端的实验中,我故意在一个类的注释里写上“// 可能的扩展:集成缓存层”,然后让Codex重构。结果它在80%的情况下都会注入一个缓存相关的接口,即使我在提示词里明确说“不要添加注释里提到的扩展功能”。
注释对Codex的影响远比我们想象的更大,尤其是包含“可能需要”、“计划支持”、“TODO”等关键词的注释。Codex似乎在训练中学到了“TODO应该被实现”的模式。
3.6 机制六:测试代码的“反向污染”
这个问题出现在我让Codex分析项目的测试代码来理解业务逻辑时。
在CMS系统重构中,我为了帮助Codex更好地理解业务,把测试文件也作为上下文提供给了它。结果,测试代码中大量的Mock对象设置和依赖桩,被Codex理解为“这个类在工作中需要这些依赖”。
举例来说,ArticleQueryServiceTest中Mock了CacheManager来确保测试的隔离性,即使真实代码里ArticleQueryService并不直接依赖CacheManager(它是通过Repository内部的二级缓存实现的)。Codex看到测试里反复出现CacheManager,就推断这个类是核心依赖,把它加进了生产代码的构造函数里。
在提供上下文给Codex时,测试代码可能是一把双刃剑,它有助于理解业务逻辑,但也可能引入测试桩的反向污染。
四、识别体系:如何在Codex生成的代码中快速发现不必要依赖
理解了Codex为什么会制造不必要依赖之后,接下来需要一套可靠的识别方法。我在三次重构项目后总结出了一套“三层过滤法”,可以在代码审查阶段快速发现90%以上的不必要依赖。
4.1 第一层过滤:构造函数参数使用率审查
这是最直接、最机械、但对Codex生成的代码最有效的方法。
操作方法:
- 打开一个Codex生成的类
- 读取构造函数中的所有注入参数
- 在每个参数对应的字段名上使用IDE的“查找引用”功能
- 如果某个字段在当前类中没有任何方法调用它,标记为“疑似不必要依赖”
我在支付系统重构后做了这个审查,结果如下:
| 类名 | 构造函数参数数 | 被实际调用的依赖数 | 不必要依赖数 | 不必要依赖占比 |
|---|---|---|---|---|
| PaymentProcessor | 8 | 3 | 5 | 62.5% |
| PaymentNotificationBuilder | 5 | 0 | 5 | 100% |
| OrderStateManager | 6 | 4 | 2 | 33.3% |
| PaymentValidationService | 4 | 2 | 2 | 50% |
| Average | 5.75 | 2.25 | 3.5 | 60.9% |
60%的注入依赖完全没有被调用,这不是偶然,这是Codex在缺乏约束时的典型表现。
但要注意: 这个方法有一个边界情况。有些依赖虽然当前没有被直接调用,但通过框架机制(如AOP拦截器、中间件、过滤器)被间接使用。例如,ILogger<T>在有些框架中是通过拦截器自动记录方法出入参的。这种情况下需要具体判断。我的经验法则:如果这个依赖可以通过框架层的AOP或中间件处理,那就不要注入到具体的类中。
4.2 第二层过滤:方法行为与依赖目的的匹配分析
有些依赖虽然被调用了,但调用方式暴露了“这个依赖本不需要在这里”。第二轮过滤就是检查这些“使用但不合理”的依赖。
我在CMS系统重构中发现了一个典型案例:
public class ArticleQueryService {
private final ArticleRepository articleRepository;
private final ContentFormatter contentFormatter;
private final MetricsService metricsService;
public ArticleDTO getArticleById(String id) {
Article article = articleRepository.findById(id)
.orElseThrow(() -> new ArticleNotFoundException(id));
ArticleDTO dto = contentFormatter.format(article);
// 这里MetricsService被调用了
metricsService.incrementCounter("article.query.count", 1);
return dto;
}
}
MetricsService被调用了,看起来是必要依赖。但仔细分析:这是一个纯查询方法,埋点逻辑不应该耦合在业务代码里。正确做法是用AOP、中间件或装饰器模式处理横切关注点。Codex不了解这种架构分层原则,它只是“看到代码里有埋点,就把MetricsService作为依赖注入”。
第二层过滤的核心判断标准:这个依赖执行的操作,是业务逻辑的核心部分,还是可以分离的横切关注点? 如果是后者,就不应该直接注入。
常见的横切关注点包括:
- 日志记录
- 性能监控和埋点
- 事务管理
- 安全检查(除非Method级别的细粒度授权)
- 异常转换和格式化
4.3 第三层过滤:依赖链的传播深度检查
这是最深层的过滤。有时候一个依赖看起来是必要的,它被调用了,而且是业务逻辑的核心部分。但是,如果这个依赖的获取需要很深的传播链(即通过多个其他依赖才能获取到),可能意味着架构有问题。
我在物流对账系统中遇到的问题:
public class ReconciliationService {
private readonly IOrderService orderService;
public void reconcileOrder(string orderId) {
// 通过orderService获取客户信息
var customer = orderService.GetOrderById(orderId)
.Customer;
// 通过客户获取其合同信息
var contract = customer.GetActiveContract();
// 通过合同获取结算汇率
var exchangeRate = contract.GetExchangeRate();
// 这才是真正需要的
DoReconciliation(orderId, exchangeRate);
}
}
表面看,ReconciliationService正确依赖了IOrderService。但实际上,它真正需要的是exchangeRate,而获取这个值的路径是OrderService → Order → Customer → Contract → ExchangeRate。这违反了“依赖倒置原则”,高层模块(ReconciliationService)通过底层模块的嵌套获取到了它不应该知道的信息。
Codex在生成这段代码时,选择了最容易生成的路径:“既然需要汇率,从订单开始顺藤摸瓜往下找就行了”。它没有进行依赖层级分析。
第三层过滤的判断标准:这个依赖的API返回值,是否暴露了不该由当前层知道的底层细节? 如果是,应该注入一个更高层级的抽象(如直接注入IExchangeRateProvider),而不是让当前类通过长链式调用获取它所需要的信息。

五、解决方案:让Codex生成干净依赖的完整工作流
识别问题是第一步,真正有价值的是:如何在使用Codex重构时,从一开始就减少不必要依赖的产生?以下是我经过三次大型重构迭代出来的一套工作流,每一步都有具体的提示词模板和验证方法。
5.1 第一步:定义“依赖白名单”约束文件
这是最重要的预防措施。在开始重构之前,为项目创建一个依赖约束文件,明确告知Codex哪些依赖是允许的、哪些是需要特殊理由的、哪些是禁止的。
我在支付系统重构中使用的约束文件(精简版):
## 项目依赖注入约束规则 允许的依赖(无需特别说明) ILogger<T>: 仅在T是入口控制器或顶层编排服务时允许 IOptions<T>: 仅在需要读取配置的类中允许 业务Repository接口: 仅在数据访问层允许 需要特殊理由的依赖(必须在注释中说明用途) IDistributedCache: 需要说明缓存策略和key设计 IMessageBus/IEventBus: 需要说明发布的事件和订阅者 IMetricsCollector: 需要通过AOP实现,禁止直接注入业务类 禁止的依赖 IServiceProvider及任何Service Locator模式 具体的实现类(必须面向接口) 任何不在当前限界上下文中的服务接口
效果: 在提供这个约束文件后,Codex生成的PaymentProcessor的依赖从8个减少到4个(仍然有1个不必要的,但大幅改善)。没有约束文件的情况下,不必要的依赖占比是60.9%;有约束文件后,这个比例降到了28%。
关键细节: 这个约束文件必须放在Codex容易注意到的位置。我推荐的做法是:
- 在项目根目录创建
.ai/constraints.md - 在每个需要重构的类文件顶部,用注释引用:
// @ai-constraints: .ai/constraints.md - 在每次Codex会话开始时,明确提示:“在生成代码前,请先阅读并遵守项目依赖约束文件”
5.2 第二步:设计“按需注入”提示词模板
即使有了约束文件,Codex在实际生成时仍会“发挥”。我设计了几个专用的提示词模板,针对不同的重构场景。
模板一:类拆分时的提示词
## 任务:将以下类拆分为多个单一职责的类 类:[ClassName] 约束条件: 每个新类的构造函数只注入它直接调用的依赖 如果原始类有10个依赖,不代表每个拆分后的类都需要这些依赖 在每个新类的方法实现完成后,分析该方法的控制流和数据流 移除任何在方法中未被调用的依赖 不要根据类名推断依赖 - 只根据方法实现决定依赖 输出格式: 对每个新类,在构造函数上方用注释列出每个依赖的用途。 格式:// Dependency: [InterfaceName] - Purpose: [具体用途] 原始代码: [粘贴代码]
模板二:为新类生成依赖时的提示词
## 任务:为 [ClassName] 设计依赖注入构造函数 类的方法签名和行为: [粘贴方法签名和简要说明] 生成规则: 只注入在方法实现中被直接使用的依赖 不要注入父类或相似类的依赖 如果方法只返回硬编码常量或进行纯计算,不需要任何依赖 横切关注点(日志、监控、缓存)不要作为直接依赖注入 使用最抽象的接口,而非具体的实现类 对每个注入的依赖,回答: 这个依赖在哪个具体方法中被使用? 如果不注入这个依赖,这个方法能否工作?
模板三:重构后审查的提示词
## 任务:审查以下类的依赖注入是否合理 类代码: [粘贴完整代码] 审查标准: 每个构造函数的参数是否在当前类中被直接调用? 是否存在仅通过链式调用才需要的间接依赖? 是否存在可以通过AOP或中间件处理的横切关注点? 是否存在根据类名推断而非实际需要的依赖? 输出格式: 必要依赖:[列出] 可疑依赖:[列出及原因] 建议移除:[列出]
我在实际使用中,这三个模板的组合使用可以将不必要依赖的产生率降低到15%以下。剩余的15%通常需要人工判断,比如某些依赖在方法体内是通过反射或动态调用使用的,Codex和简单的引用计数都检测不到。
5.3 第三步:建立“依赖审计”检查点
即使有了约束文件和提示词,审查环节仍然不可省略。我在工作流中设计了几个硬性检查点。
检查点一:每个类的构造函数参数数量上限
我给自己定了一条规则:由Codex生成的类,构造函数参数不得超过4个。如果超过,必须拆分为更小的类,或者说明为什么多个依赖无法合并。
这不是说4个以上就是错的,但Codex生成的代码中,依赖数量往往是膨胀的信号。我在支付系统中发现,Codex生成的超过4个参数的构造函数,70%包含不必要依赖。而在手工编写的代码中,符合单一职责的类很少有超过4个依赖的情况(除非是Facade或编排器)。
检查点二:依赖的依赖图分析
我会使用依赖分析工具(如.NET的DependencyGraph、Java的jdeps、Python的pydeps)生成每个模块的依赖图,然后重点检查Codex重构过的模块。
在CMS系统案例中,依赖图显示ArticleQueryService与SearchIndexer有一条连线,这在业务逻辑上毫无意义。这个可视化检查在一秒钟内就暴露了问题。

5.4 第四步:设计“最小化接口”的代码模板
这是一种更主动的策略,不是事后审查,而是一开始就要求Codex使用“最小化接口”模式生成代码。
传统方式中,我们会定义一个全功能的接口然后注入:
public interface IOrderRepository {
Order GetById(string id);
void Save(Order order);
void Delete(string id);
List<Order> Search(OrderSearchCriteria criteria);
void UpdateStatus(string orderId, OrderStatus status);
// ... 15个其他方法
}
public class PaymentService {
private readonly IOrderRepository _orderRepo;
// PaymentService只需要GetById和UpdateStatus
// 但它却依赖了整个IOrderRepository的20个方法
}
这在技术上没问题(接口隔离原则允许实现类提供更多方法),但在语义上,PaymentService对IOrderRepository的所有方法都有了“依赖声明”。Codex看到这个关系,就会在分析依赖时认为“这个类可能需要使用IOrderRepository的任何方法”。
最小化接口策略:
public interface IOrderReader {
Order GetById(string id);
}
public interface IOrderStatusUpdater {
void UpdateStatus(string orderId, OrderStatus status);
}
public class PaymentService {
private readonly IOrderReader _orderReader;
private readonly IOrderStatusUpdater _orderStatusUpdater;
// 依赖非常明确:只读取订单和更新状态
// Codex不会为这个类推断出搜索或删除的能力
}
我在提示词中加入这个策略后,Codex生成的代码中,不必要依赖进一步降低了约8个百分点。“更小的接口”向Codex传递了更强的类型约束信号,限制了它的模式补全空间。
六、不同情况的取舍:何时容忍、何时清零
在实践中,我意识到追求“零不必要依赖”并不总是最优解。以下是我根据不同场景总结的取舍框架。
6.1 容忍场景:快速原型和一次性迁移脚本
在物流对账系统的案例中,有一部分是一次性数据迁移脚本。这些脚本只在系统升级时运行一次,用完即弃。
对于这类代码,我的策略是:只要依赖注入不影响功能正确性,可以容忍一定程度的不必要依赖。 因为审查和清理这些脚本的时间成本可能超过它们实际带来的风险。我的经验阈值是:生命周期少于一周的代码,不必要依赖容忍度可以提高到20%。
6.2 严格控制场景:核心业务服务和长期维护的模块
相反,支付系统的核心模块属于“绝对不能出问题”的类型。对于这类代码,我的策略是不接受任何不必要依赖,每个注入的依赖都必须有清晰的业务理由,并且在代码注释中可见。
长期维护的模块最怕的就是“依赖债务复利”,今天的一个不必要依赖,三个月后会被另一个开发者使用,变成“事实必要”依赖,然后限制重构的可选路径。我在CMS系统上看到这个循环已经开始了:Codex引入的SearchIndexer依赖在两个月后被一个新功能使用了,不是因为那个功能需要搜索索引,而是因为“这个依赖既然已经存在,直接用着方便”。
6.3 技术债务的“依赖预算”管理
参考财政预算的概念,我对每个重构模块设定了“依赖预算”:
- 服务类: 依赖预算上限 = 4个
- 编排类(Facade): 依赖预算上限 = 6个
- 工具类/辅助类: 依赖预算上限 = 2个
- DTO/Value Object: 依赖预算 = 0个
如果一个类超过了预算,触发一次架构审查,不是绝对禁止,但需要明确的理由。这套机制在我的团队里运行了半年,有效遏制了Codex的“依赖膨胀”倾向。
三个等级的应对策略:
| 场景 | 不必要依赖容忍线 | 审查频率 | 清理策略 |
|---|---|---|---|
| 一次性迁移脚本 | ≤20% | 仅功能验证时检查 | 不主动清理,除非引发问题 |
| 内部工具/管理后台 | ≤10% |
读者评论
读完深有同感。我们团队去年用Codex重构一个CRM系统,也是发现AI特别喜欢把父类的所有依赖复制到每个子类里。一个只负责格式化日期的类,竟然注入了邮件服务和缓存客户端。后来我干脆在重构前先用脚本把旧类里的依赖列成白名单,提示词里明确写‘只保留本方法实际调用的依赖’,才勉强控制住。文章里那个‘继承性依赖污染’的描述太精准了。
这篇文章的数据太有说服力了,三次重构不必要依赖占比都超50%,支付系统冷启动从3.2秒涨到11.8秒。我原先觉得多几个依赖无所谓,直到上个月我们的API网关因为DI容器解析过多未使用依赖导致启动超时。现在每次AI生成代码后,我都会用工具扫描构造函数里的注入项,和实际代码调用做交叉比对。作者说的‘宁多勿漏’安全策略确实是根因。
写得非常真实,尤其是‘过度关联’那段。Codex看到一个类里有订单和支付,就自动关联了全文检索服务,这种跨模块的脑补太常见了。我补充一点:后来我测试发现,给Codex提供被重构类的UML类图或依赖关系描述文件,能显著减少这种错误推断。AI不是不懂业务,而是它没有上下文边界。建议文章可以加一条:重构前先手动画出当前依赖图谱作为提示词的一部分。
作者提到的‘单元测试成本爆炸’我深有体会。之前重构一个物流模块,Codex给每个类都注入了ILogger和IMetrics,结果写测试时Mock了20多个根本没被调用的服务。后来我强制要求Codex在构造函数里只保留那些在方法体里显式出现的依赖,并在代码注释里注明每个依赖的用途。不过这个流程靠人工审查还是累,期待能有自动化的依赖审计工具。
文章里那张雷达图很有启发,Codex在代码模式一致性上得分9,但实际必要性只有3。这让我意识到问题不是AI能力不足,而是评估维度偏差。我们在用AI重构时,往往被它的整洁代码迷惑,忽略了隐藏的依赖负担。建议团队建立重构后的代码质量检查清单,重点包括:所有注入依赖是否被当前类直接调用?是否存在未使用的构造函数参数?冷启动时间是否异常?数据驱动的复盘方式值得推广。