功能越迭代越多时,系统隐性复杂度是怎样一步步被堆到失控的

你可能常有这种感受:
版本一版接一版发,功能一块块加上去,线上没出大事故,但日常开发越来越折腾:

改个小需求要翻半天代码;
排查一个小问题得拉好几个人上语音;
谁提“重构一下”都知道难度很大。

这不是你矫情,往往说明一件事:
系统的隐性复杂度,已经在多年迭代中悄悄堆到了边缘。

这篇文章只讲三件事:
一是复杂度到底是怎么被一点点堆出来的;
二是有哪些明显信号在提醒“快失控了”;
三是还能通过哪些简单动作,把复杂度拉回相对可控的范围。

==============================

一、舒适期里,复杂度在悄悄攒

早期系统通常比较“听话”:

  • 新功能加得快;
  • 问题多半能直接定位到某个模块;
  • 相关的人都比较熟代码,脑子里有一张差不多的架构图。

正因为一切还撑得住,
大家容易形成一种错觉:“先干活要紧,复杂度以后再管”。

但每一次“先凑合一下”都在往里塞东西:

  • 这里多一个 if 特判;
  • 那里多几个状态字段;
  • 新需求先挂在老链路上。

短时间看不出问题,几年之后就变成谁也不敢动的大泥球。

==============================

二、隐性复杂度是怎么被一点点堆出来的

1、特例越来越多,主流程被淹没

典型演进路径:

  • 先实现一条“标准流程”;
  • 业务说这里有个例外,加一个判断;
  • 又来一个例外,再叠一个条件;
  • 久而久之,“例外”比“主流程”还长。

后果是:

  • 文档里的规则和代码里的实现逐渐对不上;
  • 谁都不敢删旧分支;
  • 改一处行为,很难保证别的路径不跟着变。

复杂度不在功能数量上,而在这些看似无害的特判组合里。

2、状态乱长,没人再敢碰数据结构

随着需求增加,状态会这样膨胀:

  • 表和对象上不断加字段、枚举、标记;
  • 不同模块对同一字段的理解并不一致;
  • 很多状态组合从来没认真设计过。

于是:

  • 一个行为是否生效,要看 3–5 个字段组合;
  • 某些诡异 bug 只在极少数状态下出现;
  • 谁也不敢删字段,只能继续往上加。

看上去只是“多了几个字段”,
实际是状态空间指数级膨胀。

3、依赖链条被一点点拉长

出于“复用”和“解耦”,系统之间的调用会越来越密:

  • A 服务调 B 服务,B 又调 C;
  • 前面加网关,后面挂规则引擎,中间插埋点与缓存;
  • 旁边还绕了一圈代理、代理出口和各种中间件。

每一步都能给出合理解释,
叠在一起就是:

  • 任何一次请求的真实路径很难说清;
  • 任意一环抖一下,整个链路都跟着不稳;
  • 故障定位从“找一段代码”变成“先猜哪一层出了问题”。

==============================

三、哪些信号说明复杂度已经接近失控

出现下面这些情况,就该警惕了:

1、小需求改动越来越“重”

  • 改一个小按钮,评审要半天;
  • 字段扩展牵出一堆“历史兼容”讨论;
  • 真正写代码的时间很少,大部分时间花在“怕出事”。

这说明不是需求难,而是系统对改动极其敏感。

2、排查严重依赖“老人记忆”

  • 新人看代码看不出意图,只能问“当时为什么这么写”;
  • 很多逻辑只能用“那次事故之后加的兜底”来解释;
  • 一两个老同事一离职,模块就没人敢动。

系统真实设计不在文档和图里,只在少数人口述里。

3、修 bug 主要靠“再加个条件”

遇到问题时团队的默认做法是:

  • 再加一个 if;
  • 再多一段兜底;
  • 先把现象挡掉再说。

很少有人提出:

  • 把这段流程抽出来重画;
  • 把状态重新分组建模;
  • 删除几条过期逻辑再实现新规则。

补丁越打越多,复杂度只会持续上升。

==============================

四、怎么把复杂度往回拉一点

1、先画出“真实现在”的关键链路

不要一上来就谈“目标架构”,先搞清楚现在关键链路到底长什么样。

从一两条核心场景开始:

  • 下单全链路
  • 登录与权限
  • 数据采集配合代理出口那条链路

对每条链路至少要弄明白:

  • 经过哪些服务和关键模块
  • 受哪些配置和开关影响
  • 中间依赖了哪些外部系统

画出来之后,很多“怎么这么绕”的地方会暴露出来,后面重构才有具体落点。

2、给复杂度设“红线”,不是无限透支

可以定一些简单的团队规则,比如:

  • 单个模块的条件分支数超过某个值,就必须考虑拆分
  • 核心路径上的依赖层数超过某个上限,就要合并或收口
  • 对核心数据结构,新增状态前必须讨论“能不能改模型而不是加标记”

不是要追求完美设计,而是让每次“再凑合一把”之前,大家至少意识到在透支复杂度。

3、每个迭代顺手做一点“减法”

一次性重写全系统很不现实,更可行的做法是每个迭代顺手还一点债:

  • 清掉确认不用的配置与开关
  • 删除明显死分支和重复逻辑
  • 把重复出现的校验和转换抽成统一模块
  • 对最常出问题的那条链路,花一个迭代单独梳理

这些“小减法”不会立刻把系统变干净,但会慢慢阻止复杂度继续失控地堆上去。

4、用易路把“出口层复杂度”先收一收

很多系统的隐性复杂度,其实是被出口层放大的:
不同脚本各写一套代理接入方式、不同环境各自维护 IP 列表、有人手动切线有人写死在配置里,最后谁也说不清“这条业务现在到底走哪”。

这块反而是最容易做减法的地方,你可以把“代理出口”统一收敛到易路代理上来做一层:

  • 把按环境、按业务、按敏感级别分散的出口,全部整理成几类清晰的线路组和标签
    例如:核心业务组、运营组、采集组、测试组
  • 让脚本和服务只认标签不认 IP,后续换线、扩容、降级都在易路面板里改,不再动一堆零散配置
  • 通过易路后台的延迟、成功率、错误率统计,替代掉自家系统里一堆临时埋点和脚本式排查

对你来说,这相当于给“出口层复杂度”来了一次集中收口:
接入方式统一、线路分组统一、观测入口统一,后面每次加功能、加脚本,只需要接在既有标签和线路组上,不再额外堆一层“出口玩法”。

==============================

功能越迭代越多,复杂度上升是自然结果
真正危险的是团队习惯了“先加再说、不敢删、不敢改”,
久而久之谁都知道系统很复杂,却没人说得清“复杂到哪了”。

当你发现:

  • 压测都能过,真业务一上量就各种抖;
  • 排查越来越依赖几个人的口头记忆;
  • 改动任何地方都像在拆炸弹,

基本就可以确认,隐性复杂度已经到了该认真管理的时候。

从今天起,可以先做三件简单的事:

  • 给任务按业务价值分级,别再一视同仁;
  • 画出一两条关键链路的真实结构图;
  • 在评审时固定问一句“这次改动会不会再推高复杂度”。

只要这三件事能持续做下去,
系统就会从“越来越难碰”,
慢慢变回“虽然复杂,但至少看得见、说得清、改得动”的状态