1. 起点:为什么“熵”救不了我们的系统
1.1 那个熵值很低的系统,却没人敢动它
两年前我接手了一个遗留系统,如果你只看指标,会觉得它简直是无懈可击的“模范生”:提交历史非常规律,每个文件每两周才被碰一次,提交熵低得离谱;圈复杂度平均不到3;依赖矩阵算出来的归一化熵也只有0.2出头。任何一门讲软件度量的课程看到这组数据,都会给出“健康系统”的结论。
但真实情况是,这个系统只要我想动任何一个核心模块,周边至少五个模块会跟着出问题。改一个配置项,要排查三层继承;加一个字段,要同步四个数据模型。整个团队没人愿意碰它,每次迭代都绕着它走。我开始怀疑,是不是我用来度量“复杂度”的这套工具本身就出了问题。
后来我把这个问题想明白了:传统熵度量捕捉的是“无序”,但这个系统的问题根本不是“乱”,而是“过度且不合理的结构性”——它有大量层级、大量接口、大量隐秘交互,这些都被熵指标天然屏蔽了。这种隐藏在整齐表面之下的深层复杂度,是熵看不到的。这就是我这一年多一直在琢磨的“从Entropy到Epiplexity”这个转变的来源。
1.2 传统熵指标在软件系统中的三种算法与失效场景
先说清楚我用的三种主流熵指标是什么、怎么算,以及它们各自在什么场景下失效。这不是为了否定它们,而是为了知道它们的能力边界。
提交熵(Commit Entropy),它的计算方式是基于版本历史,把每个文件作为一个“符号”,统计它在时间窗口内被修改的次数分布,带入香农熵公式:
H = -Σ p(i) * log2(p(i))
其中p(i)是第i个文件在窗口内修改次数占全部修改次数的比例。提交熵越低,意味着修改越集中或越均匀(取决于你的归一化方式)。问题在于:它完全无法区分“一个人常年锁死某个文件不许别人动”和“文件真稳定”。某电商后台就是这种情况——核心文件被一个老同事长期独占修改,提交熵低得好看,但团队知识断了层,谁接手谁崩溃。
圈复杂度,它衡量的是控制流中独立路径的数量。圈复杂度低意味着分支少、逻辑直白。但它只看单个函数内部,完全不看函数之间的交互。我见过一个服务,所有函数圈复杂度都不超过4,但函数之间通过共享可变状态互相纠缠,调用顺序稍一变就全盘错乱。这种系统用圈复杂度度量是满分,用起来是噩梦。
依赖矩阵熵,把模块间的依赖关系编成矩阵,计算矩阵的信息熵。它能反映依赖是否均匀分散,但同样有个盲区:它只看“有没有依赖关系”,不看依赖的方向和依赖的深度。一个A→B→C→A的循环依赖,和一棵A依赖B、B依赖C的干净树形结构,在依赖矩阵熵上可能极为接近。可这两者的维护代价完全是天壤之别。
看完这三个失效案例,你会发现一个共同点:它们都在测量“这个系统有多混乱”,但没人回答“这个系统的结构本身要付出多大代价”。而结构付出的代价,才是一个系统真正让人头秃的地方。
2. Epiplexity:从无序度走向“表层复杂性”
2.1 为什么需要造一个新词:熵回答不了“结构的代价”
我在内部复盘的文档里第一次写下“Epiplexity”这个词时,其实只是临时起意——它就是“表层复杂性之上涌现出的结构性复杂度”。词根上,epi-是“在……之上”,plexity取自complexity。我想表达的是:当信息熵已经把所有无序的部分剥离掉之后,系统剩下的、由层级结构和交互关系产生的那部分代价,才是更值得我们盯住的东西。
举例来说,一个系统可能每个文件都短小精悍,每个函数都只做一件事,依赖关系也都清晰可见。这一切听起来无比完美。但如果你把这些模块组织成六层抽象,每一层都要做一次数据转换,那一个请求从入口到数据库要经过十一次对象拷贝。这个系统在熵指标下是“健康”的,但它的真实复杂度高得吓人。传统的复杂性研究往往把“熵”和“复杂”混在一起讨论,这导致了很多实践中的误判:以为熵降下来了,系统就变简单了。实际上,降熵和降低结构复杂度是两码事。
我在实际项目中把Epiplexity定位成一个操作性定义,而不是一个严格的学术概念。它的核心问题是:一个系统为了实现某个功能,额外付出了多少结构上的代价?它跟“乱不乱”无关,只跟“为了维持某种秩序,我们搭了多少架子”有关。
2.2 Epiplexity的五个核心维度
为了让这个概念能落地,我把Epiplexity拆成五个维度,每个维度都有明确的观察对象和现实意义。
维度一:层级冗余度。指一个调用从最外层到最内层要穿透的抽象层数。这个不是简单的“多层就好或不好”,而是看层数和它所承担的职责是否匹配。一个订单流程要经过Controller、Service、Manager、DAO、Helper五层,每层都只做简单的透传,那这些层就是纯成本。相反,每一层都有独立的业务规则,那这个层级深度是合理的。
维度二:交互非对称度。指依赖关系是不是“单向干净”的。理想情况下,依赖应该是分层的:上层依赖下层,下层不反向依赖上层。现实系统里经常出现一个模块同时被几十个模块依赖,而它又反过去依赖其中几个,形成复杂的双向交互。这个维度关注的就是依赖方向上的“非对称性”,越不对称越健康,越对称越危险。
维度三:循环结构占比。指整个依赖图中有多少模块处于循环依赖里。循环依赖是结构复杂度的最强信号,因为它意味着在这个环路里的任何改动都可能产生跨越多个模块的连锁反应。快而准的统计方式是把依赖图做拓扑排序,那些无法被排进序列的模块,就是循环的参与者。
维度四:信息流路径长度。一个业务数据从产生到落地,平均要经过多少跳转。路径越长,数据出错的概率越大,调试时需要跟踪的上下文就越多。这个指标跟网络分析里的“平均路径长度”很像,但它不是网络节点的物理距离,而是数据在不同模块间被搬运的次数。
维度五:边界信号密度。指模块对外暴露的接口数量以及每个接口的输入参数组合空间。接口越多、参数越灵活,这个模块的“边界”就越复杂。我见过一个内部工具类,对外提供了四十多个public方法,每个方法都有三个以上可选参数。它本身的代码很简单,但任何人想正确调用它,都要读很久文档——这就是典型的“高边界复杂度”。
这五个维度合在一起,才构成我理解的Epiplexity。它跟熵最大的不同是:熵是统计层面的事,Epiplexity是结构层面的事。
2.3 和熵的关系:不是替代,是分层的度量
我经常被问到:你说了熵这么多缺点,是不是要抛弃熵?不是。我的观点很明确——熵和Epiplexity是两个不同层次的问题,两者需要配合使用。
我整理过一个对比表,基本可以说明两者在我的工作里的分工:
| 对比项 | Entropy(熵型度量) | Epiplexity(表观复杂性) |
|---|---|---|
| 度量对象 | 无序度、随机性、分布均匀性 | 结构性、层级性、交互关系 |
| 回答的问题 | 系统有多乱? | 系统的结构代价有多大? |
| 低值含义 | 可能是真有序,也可能是单一来源的伪有序 | 结构简单,维护成本低(相对而言) |
| 高值含义 | 修改分散、变化混乱 | 链路深、循环多、边界复杂 |
| 典型手段 | 香农熵、圈复杂度、矩阵熵 | 依赖图分析、路径追踪、边界统计 |
| 失效场景 | 表面整洁但盘根错节 | 极少单独失效,但需结合熵判断“乱”还是“贵” |
熵告诉我们“变化集中在哪、分布是否均匀”,Epiplexity告诉我们“结构本身要付多少代价”。一个高熵系统不一定贵,可能它只是被频繁修改但结构很扁平;一个低熵系统也不一定便宜,可能它只是没人动,但骨架重得像一座堡垒。只有当两个指标一起看时,才能判断一个系统的真实处境。
3. 落地:把Epiplexity量化成一组可计算的指标
3.1 五个维度的测量方式与数据来源
光谈概念没有用,关键是能不能落地。这半年我陆续整理出一套可执行的度量流程,每个维度都有明确的数据来源和近似算法。
层级冗余度,我用的数据来源是调用链追踪。对核心业务链路做一次抽样,记录每个请求从入口到出口经过的模块总数,取P50和P90。如果一个交易的完整调用链平均穿透超过8层,且其中超过一半的层级只是透传(没有业务逻辑、没有数据转换、没有校验),那这个系统的层级冗余度就超标了。透传的判断方式很直接:看这几个模块的代码变更频率是不是远低于其他模块,并且函数体里没有独立逻辑。
交互非对称度,数据来源是静态依赖分析。我写了一个简单的脚本,把每个模块的import/依赖关系解析出来,组成有序对(A, B)。然后计算:
非对称度 = 1 - (双向依赖对数 / 总依赖对数)
双向依赖对就是既存在A依赖B,也存在B依赖A的两个模块。这个值越接近1,依赖方向越干净;越接近0,模块间越容易互相牵扯。现实中一个模块A依赖B,B也依赖A,往往不是真的需要互相引用,而是共享代码放错了位置。
循环结构占比,我直接用Kahn拓扑排序算法。把所有模块按依赖图排序,能排进去的是一次性可解耦的,排不进去的就是循环依赖的参与者。占比计算:
循环占比 = 无法进入拓扑序的模块数 / 总模块数
我在几个遗留项目上跑过,循环占比超过20%的系统,改动一个接口平均要回归测试三个以上模块,这个相关性非常强。
信息流路径长度,这个稍微麻烦一点。我是在数据访问层埋点,记录一个业务实体在内存中被传递、复制、转换的次数。比如用户信息从Controller接收,转为DTO,再转成Entity,再传给第三方SDK,这就算四跳。统计核心业务实体的平均跳数和P95跳数。路径越长,意味着数据一致性出问题的概率越大。
边界信号密度,数据来源是公开API列表。做法很简单:统计每个模块对外开放的方法数、参数个数、参数类型组合数,然后按模块归一化。核心计算公式是:
边界复杂度 = 公开方法数 * log2(平均参数组合数 + 1)
这个公式是经验式,不是理论推导,但它能很好地区分“一个简单模块”和“一个看起来简单但用起来很麻烦的模块”。
3.2 一个最小可用实现(伪代码)
为了让你能快速把这个方法跑起来,我给出一段核心思路的伪代码,你可以根据自己的工程环境换成对应语言实现。这里我用的是Python风格来描述,不依赖特定框架。
def calc_epiplexity(modules, dependencies): epiplexity = {} # 维度1: 层级冗余度 for trace in call_traces: depth = len(trace) passthrough = count_passthrough_layers(trace) epiplexity['depth_p50'] = percentile(depth, 50) epiplexity['passthrough_ratio'] = passthrough / depth # 维度2: 交互非对称度 dep_pairs = set() bi_pairs = set() for a, b in dependencies: dep_pairs.add((a, b)) for a, b in dep_pairs: if (b, a) in dep_pairs: bi_pairs.add((a, b)) epiplexity['asymmetry'] = 1 - len(bi_pairs) / max(1, len(dep_pairs)) # 维度3: 循环结构占比 cycle_modules = find_cycle_participants(dependencies) epiplexity['cycle_ratio'] = len(cycle_modules) / len(modules) # 维度4: 信息流路径长度 path_lengths = [] for entity in core_entities: path_lengths.append(trace_data_flow(entity)) epiplexity['flow_path_p50'] = percentile(path_lengths, 50) epiplexity['flow_path_p95'] = percentile(path_lengths, 95) # 维度5: 边界信号密度 boundary_scores = [] for m in modules: methods = get_public_methods(m) param_combos = get_avg_param_combos(m) boundary_scores.append(len(methods) * log2(param_combos + 1)) epiplexity['boundary_avg'] = mean(boundary_scores) return epiplexity实际使用的时候,前三项可以依赖现成的静态分析工具,第四项需要一点埋点改造,第五项通过反射或者接口文档解析就能拿到。整个过程一周内可以搭出初版,后续根据业务特征再调整。
提示:不要一上来就追求完整自动化。先手工抽三个核心模块算一遍,理解每个指标在你系统里的真实含义,再扩大覆盖面。否则你可能会被一堆无效数据淹没,却不知道它们意味着什么。
3.3 阈值先别求全局,先用相对排序
这是我踩过最大的坑之一。刚设计出这套指标的时候,我非常想让每个维度都有一个“标准答案”,比如“层级冗余度不能超过6”“循环占比不能高于15%”。但很快我就发现,不同业务系统之间的差异远大于共同点。一个支付核心模块和一个报表展示模块,它们的合理层级深度、合理交互密度完全不同,用同一个阈值去卡,结果必然荒谬。
正确做法是先做相对排序:把所有核心模块的五个维度都算出来,按风险程度排出 Top 10。找出那些在多个维度上同时偏高的模块,再去人工读代码确认。先让指标帮你缩小排查范围,而不是让指标直接替你做判断。阈值作为参考,但不要作为“一刀切”的标准。
举个例子,某后台系统有四十多个模块,我一轮算完,发现有三个模块同时在层级冗余、循环占比、边界密度三个维度排名前五。我没急着改代码,先把这三个模块画成依赖图,手动一盯,发现它们的问题根源惊人一致:公共代码被拆得太碎,散落在多个层里,导致模块彼此都要跨层引用。这个结论靠任何一个维度单独看都是模糊的,但五个维度叠加,画面一下就清晰了。
4. 实测案例:三个模块的熵与Epiplexity画像
4.1 模块A:低熵高Epiplexity,典型的“表面整洁”
模块A是我们老系统里最稳定的一块,一年难改几次。按提交熵算是全系统最低的,圈复杂度也低得让人放心。如果只看熵,它就是“优质资产”。但我用Epiplexity这套维度一测,就露出了马脚:它的调用链平均深度是9层,其中有6层只是透传;循环依赖参与者占总模块数的12%,它自己也牵涉其中;核心数据实体平均要经过7次拷贝才能最终落库。
为什么这么多年没人发现?因为模块A的稳定来自于“没人愿意改动它”——每次改动都要测试一圈跟它纠缠在一起的模块。熵值低,不是因为它结构好,而是因为它已经被冻住了。这种模块其实是“高维护成本但低变更频率”的特洛伊木马,一旦业务要求它快速变化,整个系统就会被拖入泥潭。
后来我们做了一个实验:把透传层里没有独立逻辑的两层砍掉,模块A对外接口不变,但调用深度从9降到了7。改动本身不大,但重新跑完回归测试后,所有人都惊呆了——以前需要六小时才能跑完的回归,现在只要四小时。多出来的两层,看着很“规范”,实际是纯粹的损耗。
4.2 模块B:高熵高Epiplexity,终于找到了根因
模块B是团队公认的“刺头”,每次迭代都有一半的变更跟它相关,提交熵全系统最高,圈复杂度虽然不高也绝不好看。我们过去对它的结论很简单:代码太乱了,需要重构。但重构了两次,收效甚微——第一轮把函数拆得更细,结果调用链更长了;第二轮做了分层,结果循环依赖飙升。这种做法就是典型的“用眼睛看乱就动手”,完全没找到根因。
用Epiplexity的五维画像诊断之后,问题本质清晰了:模块B的循环依赖参与模块占比高达37%,并且边界信号密度是平均值的4.5倍。它对外暴露了132个公开方法,其中大量方法参数语义重叠。外部调用方不知道用哪个,于是每种组合都有人用,导致模块内部为了兼容各种调用方式塞满了分支判断。提交熵高只是症状,真正的病根是边界设计失控。
修复方案不是重写内部逻辑,而是先收敛边界:对外的132个方法合并归拢成32个,废弃掉那些从调用链上看没有任何真实使用者的兼容入口。这个动作用了两个迭代,模块B的循环依赖占比从37%降到了18%,Epiplexity各项指标回落到中位数水平。改动之后,团队的直观感受是——终于可以不用看调用链定义就猜到这个模块大概在干什么了。
4.3 模块C:低熵低Epiplexity,健康系统的参照系
模块C是我们系统中另一个极端:同样熵值很低,但它不只是“表面看起来没事”,而是真的没什么事。我特意拿它做对照组,验证这套度量方法会不会误杀健康模块。
测得结果:调用深度稳定在4层,每一层都有独立的业务职责;交互非对称度0.97;循环依赖模块数为0;核心数据实体路径平均3跳;公开方法数只有18个,参数组合模式非常清晰。它的提交熵低,是因为它真的结构稳,改一个局部不需要牵动其他区域,变更量自然小。
模块C让我意识到“低熵”本身没有意义,有意义的是“低熵是怎么来的”。如果低熵由高结构稳定性带来,那是优质资产;如果低熵由“不敢动、动不起”带来,那实际上是隐藏负债。从此之后,我在给团队做汇报时,再也不单独展示熵指标,而是把熵和Epiplexity放在一起,组成一张风险画像。当期几组核心数据对比如下:
| 模块 | 提交熵 | 循环占比 | 平均调用深度 | 边界复杂度 | 一句话诊断 |
|---|---|---|---|---|---|
| 模块A | 低 | 12% | 9层 | 中 | 表面整洁,结构沉重 |
| 模块B | 高 | 37% | 7层 | 高 | 边界失控,交互混乱 |
| 模块C | 低 | 0% | 4层 | 低 | 真健康,可作基准 |
5. 边界、避坑与后续扩展
5.1 最容易踩的三个坑
第一个坑是把Epiplexity当KPI。一旦团队开始考核这个数值,就会有人为了刷低指标去合并类、删接口,甚至把原本必要的分层逻辑硬压成三个文件。Epiplexity描述的是“结构代价”,但结构代价里有相当一部分是为了业务安全、扩展性、权限隔离而必须付出的。强压指标,等于把这些合理成本也一并砍掉,最后系统是“好看了”,但业务功能没法安全演进。我的建议是:它只作为架构评审时的参考信号,绝不要进入个人绩效表。
第二个坑是只算数值不看结构。有一次我把所有模块的Epiplexity都算出来后,发现一个模块得分极高,但打开代码一看,人家是个适配层,本身职责就是对外统一接口格式。它的“高边界复杂度”是这个模块存在的意义。数值一定要结合模块职责去解读,跳过代码直接看数,迟早得出荒谬结论。
第三个坑是忽略团队协作因素。同一套结构,一个稳定五人小组维护和频繁换人维护,实际复杂度完全不同。Epiplexity度量的是代码层面的结构,但代码的“感知复杂度”跟团队上下文强相关。我现在带团队做评估时,会把Epiplexity数据和团队熟悉度调查放在一起看,两者交集处才是真正的雷区。
5.2 什么时候不该用它
不是所有系统都值得上这套东西。一个十人小团队做的MVP原型,一个人能维护的类库,或者生命周期只有几个月的活动页面,完全没有必要算出循环占比和调用深度。这种规模的项目,结构复杂度天然处于可控区间,多花时间读代码比跑分析更划算。
另外,如果你所在的工程环境没有稳定的依赖关系管理,模块边界每天都在变动,那Epiplexity的测量结果会极不稳定,它描述的是“今天的快照”,而明天这个快照已经过期了。这种情况不如先稳住边界治理,等结构相对收敛了再谈度量。
算法的随机性同样是个问题。信息流路径长度依赖埋点采样,采样量不足会导致数值波动很大。我在落地的第二个项目里就吃过这个亏:只采了不到一千条调用链,P95路径长度在两次迭代之间跳变了30%。后来把采样量提到上万条,数值才稳定下来。
5.3 后续可以扩展的方向
这套思路目前更多用在“存量系统诊断”上,但我已经在尝试把它往前延伸到“变更影响评估”。具体做法是:在每次代码评审时,只计算本次变更涉及模块的Epiplexity增量。如果一次改动让某个模块的循环占比提高了2个百分点,那评审者就必须给出合理解释。这个方向的潜力很大,等于把结构复杂度从“事后体检”变成了“事前门禁”。
还有一个值得做的方向是时间维度:给每个模块建一条Epiplexity时间曲线,观察它是缓慢恶化还是快速恶化。缓慢恶化通常是技术债自然累积,快速恶化往往伴随着一次“图省事”的架构决策。盯住这类突变点,能帮我们复盘很多当时觉得“没事,先这样上”的决定。
最后,如果未来团队引入AI辅助编写代码,我更觉得Epiplexity这套思路会比熵更用得上。AI生成代码在圈复杂度、代码风格这类“单点指标”上通常会表现得很规矩,但在跨模块交互、公共边界设计上往往缺乏全局考虑。用Epiplexity去卡AI生成代码的结构性代价,会比人工review更早发现问题。
从Entropy到Epiplexity,本质上是一次视角的转换:不再只问“系统乱不乱”,而开始问“系统的结构有多贵”。这一年在好几个项目上尝试下来,我的体会是,熵告诉我们哪里在变,Epiplexity才告诉我们为什么变起来这么疼。前者是天气报告,后者是地质结构图。如果你也整天被那种“指标很好但维护到崩溃”的系统折磨,不妨也画一张属于你自己的地质结构图,可能问题会比你想的清晰得多。