返回文章列表

很多“可维护性优化”,只是把复杂度藏起来了

当复杂度只是从大函数搬到类层级、配置和调用链里,系统通常不会更好维护

很多团队做“可维护性优化”时,第一刀不是先问复杂度为什么会出现,而是先把代码拆开。

一个 300 行函数,拆成 12 个类;一个分支很多的流程,改成“策略 + 工厂 + 配置”;一个本来顺着调用就能看明白的逻辑,被改成事件、订阅器、规则表和几个看起来很干净的目录。

代码确实没那么挤了,单个文件也更短了,review 时甚至还会让人产生一种“这下高级了”的感觉。

但我的判断是:很多所谓可维护性优化,并没有减少复杂度,只是把复杂度从局部可见,变成了分散、跳转和隐蔽。 这类改动最常见的结果,不是系统更容易维护,而是问题更难定位、变更更难评估、新人更难理解。

可维护性的核心从来不是“文件短一点”“类多一点”,而是:当需求变了、线上出错了、边界条件出现了,团队能不能快速看见真实约束,并在有限范围内安全修改。

复杂度不会因为拆分就消失,只会换个地方待着

很多人对“可维护”的直觉过于视觉化。

他们看到大函数就不舒服,看到很多 if/else 就觉得落后,看到一个类里塞了多段业务判断就本能地想拆。于是复杂逻辑被拆成很多薄文件,条件分支被翻译成对象层级,业务规则被搬进配置,再加一点接口和命名,代码表面立刻整洁不少。

问题是,原来那 300 行代码虽然难看,但复杂度至少是摊在桌面上的。你从上往下读,能看到分支条件、共享状态、异常处理和最终结果是怎么连起来的。

一旦复杂度被拆散,情况就变了:

  • 你要顺着调用链跳 7 个文件,才能知道一个字段最终在哪被改;
  • 你得同时理解接口、实现类、注册逻辑和运行时装配,才能确认到底走的是哪条分支;
  • 你以为业务规则在代码里,结果一半在 YAML,一半在数据库,一半在某个启动时生成的映射表里。

复杂度没有减少,只是从“阅读时累一点”,变成了“定位问题时慢很多”。

而维护成本通常不是在代码整洁的那一刻产生的,是在三个月后有人改错逻辑、线上出故障、排查链路时结算的。

团队最容易误判的,是把“局部整洁”当成“整体可维护”

这类误判很常见,因为很多重构收益在短期内看上去都是真的。

比如一个包含多种订单处理分支的函数,改成下面这种结构:

Handler h = handlerFactory.get(order.type());
h.validate(order);
h.price(order);
h.persist(order);
h.notify(order);

这段代码当然比一长串分支看起来清爽。

但真正的问题不是这 5 行漂不漂亮,而是:

  1. handlerFactory 怎么决定拿哪个实现;
  2. validate/price/persist/notify 之间有没有共享前提;
  3. 不同实现之间是否允许行为漂移;
  4. 某个需求改动到底要动一处、四处,还是十几处。

如果这些问题没有被约束住,那这类“优雅结构”往往只是在把原本显式写在 if/else 里的业务差异,改写成分散在类层级里的隐式差异。

从 review 的角度看,它变干净了;从维护的角度看,它变得更依赖上下文了。

可维护性不是看单个局部是否优雅,而是看整个系统是否更容易回答“这次改动会影响哪里”。

真正决定维护成本的,通常不是代码形状,而是四件事

我更愿意用下面四个问题判断一次重构是不是让系统更可维护。

1. 问题出现时,定位路径是不是更短了

线上报一个“某类订单偶发重复发券”的问题,最有价值的不是目录结构整不整齐,而是工程师能不能快速找到:判定条件在哪、幂等保护在哪、副作用在哪触发。

如果拆分之后,排查路径从“看一个函数”变成“看接口定义、找实现类、查装配、追事件、翻配置”,那维护成本其实上升了。

2. 需求变更时,修改范围是不是更收敛了

好的抽象会让变化集中。坏的抽象会让变化扩散。

最糟糕的一类重构,是表面上把逻辑拆成了多个职责,实际上每次需求变化都得同步改:规则定义、工厂注册、默认配置、测试样例、监控埋点。文件是变小了,但变更面变大了。

这种系统看上去模块化,实际却更脆,因为每次改动都要赌自己没漏掉哪个角落。

3. 约束是不是变得更可见,而不是更隐蔽了

很多业务逻辑之所以难,不是因为代码写得丑,而是因为它本来就有很多前提:

  • 这个状态只能从 A 走到 B,不能直接到 C;
  • 这个字段只有某类客户能改;
  • 这个动作成功后必须带着另一个副作用一起发生。

如果重构之后,这些约束不再集中出现在一个地方,而是散落在多个类、注解、配置或监听器里,那么你得到的不是清晰,而是失忆风险。

4. 测试反馈是不是更贴近真实行为了

很多“可维护性优化”会顺手带来一堆很容易写的单测,因为每个类都变小了、依赖也 mock 掉了。

但单测数量增加,不代表系统更好改。

如果测试只能证明“这个类在 mock 世界里会返回预期值”,却不能覆盖真实流程里的装配关系、共享状态和时序约束,那它更多是在保护结构,不是在保护行为。

一个常见误区:为了消灭 if/else,把业务差异改写成类型系统

if/else 当然可能写得很烂,但“消灭 if/else”本身不是目标。

我见过不少系统,本来只有两三种明确分支,业务语义也很稳定,结果为了追求设计模式正确,把它们拆成策略接口、抽象基类、注册中心和扩展点。半年后类型从 3 种涨到 9 种,调用方却越来越难判断:哪些差异是真业务差异,哪些只是历史演化留下来的结构差异。

很多时候,分支多并不意味着一定要上对象模型;它只说明这里存在业务判断。你首先该做的,是分辨这些判断里哪些是稳定变化轴,哪些只是同一个流程里的条件分叉。

如果它只是一个流程中的几处条件判断,那么把它们强行“面向对象化”,很可能只是把原本一眼能看到的条件,改写成几层方法分派。

把条件藏进多态,不会让条件消失,只会让阅读者更晚意识到它存在。

另一个常见误区:把配置当成复杂度回收站

还有一种特别容易被误认为“更可维护”的做法,是把业务规则尽量配置化。

理由通常很好听:以后不用改代码了,运营可配,扩展更灵活。

但配置不是天然更便宜,它只是把复杂度从编译期挪到了运行期。

一旦规则配置开始承担过多职责,就会很快出现这些问题:

  • 配置之间存在优先级和覆盖关系,但系统里没有地方能完整看见;
  • 一次变更到底影响哪些场景,只能靠线上验证;
  • 配置值合法不代表语义正确,错误会在运行时才暴露;
  • 代码 review 变成“看不懂这份 JSON 到底意味着什么”。

如果一个规则经常变化,但变化仍然需要工程判断、联动测试和回滚预案,那它本质上还是代码问题,不会因为写进配置就突然变成低成本维护项。

过度配置化常见的代价不是“系统更灵活”,而是“系统终于谁都不敢动了”。

反例:有些抽象,确实会让系统更可维护

也不能把话说成“别抽象、别拆分”。

有些场景里,抽象不只是值得做,而且很有必要。

例如:

  • 你面对的是稳定且明确的变化轴,比如不同存储后端、不同支付渠道、不同序列化协议;
  • 这些变化在运行时真的需要替换,而不是只存在于想象中的“以后可能扩展”;
  • 每种实现都能遵守同一组强约束,而不是表面上同接口、实际上语义各异;
  • 团队边界也跟着抽象边界走,不同模块可以独立演进和测试。

这时抽象的价值,不是“代码更像教科书”,而是它真的减少了未来变更的冲突面。

同样,一个长函数如果同时承担了参数校验、业务决策、副作用编排和异常补偿,那把它拆成几个有明确边界的步骤,通常也是对的。前提是拆完以后,流程主干仍然看得见,关键约束没有被藏起来。

所以问题从来不是“拆不拆”,而是:拆完以后,复杂度是被收束了,还是只是被转移到了别的认知角落。

一个更实用的判断方法:先看未来最常见的修改,别先看今天的结构洁癖

如果我怀疑一次“可维护性重构”只是结构美化,我通常会先问三个很实际的问题:

  1. 下次产品再改这个需求,工程师最可能改哪几处;
  2. 下次线上再出错,值班的人最先该看哪条链路;
  3. 如果新人接手,他需要先理解业务规则,还是先理解框架搭起来的结构。

如果这三个问题的答案都变得更复杂了,那这次重构大概率没有改善维护性。

可维护性是面向未来修改成本的,不是面向今天代码截图的。

结尾

很多“可维护性优化”之所以危险,不是因为它们完全没用,而是因为它们太容易在局部上显得正确。

类变多了,函数变短了,目录变整齐了,review 通过得也更顺了。但真正的维护成本,来自理解、定位、修改和验证,而不是来自视觉上的整洁感。

所以我的建议很简单:别把复杂逻辑拆得看不见,先把复杂逻辑拆得改得动。

如果一次重构只是让复杂度离开了当前文件,却进入了调用链、配置层和抽象层,那它通常不是在提升可维护性,只是在延后下一次排障时的痛苦。