免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

软件架构度量:从主观体感到可量化数据的落地方法

软件架构度量:从主观体感到可量化数据的落地方法 软件架构度量这个主题最近被搜索的次数不少但搜出来的结果经常跑偏。有人搜到的是安卓应用支持的 CPU 架构查看方法有人搜到的是 TPM 安全度量过程还有人搜到的是机器学习里的等度量映射。这些和我们要讨论的“软件架构度量”完全不是一回事。这里说的软件架构度量是指对系统的模块划分、包依赖、类结构、函数复杂度、可维护性这些设计层面的东西做量化分析把“架构到底健不健康”从一种主观体感变成一组可以对比、可以跟踪、可以设门槛的数据。这篇内容适合正在带一个中型以上系统的技术负责人、架构师、资深开发也适合从业务转技术管理的同学。如果你已经明显感觉到代码越写越乱、改动越来越慢但又说不清到底乱在哪、慢在哪下面这套度量思路和落地方法可以直接照着用。1. 先对齐概念软件架构度量到底度量什么1.1 一个词三种理解先排除干扰项“架构”和“度量”单独拿出来在中文技术圈里都有多重含义。搜索软件架构度量时经常混进来三类内容先说清楚避免后面聊偏。第一类是 CPU 架构比如“如何查看一个安卓软件支持的 CPU 架构”。这是指令集层面的架构说的是 arm64-v8a、armeabi-v7a、x86_64 这类 ABI 标识。安卓模拟器兼容问题、安装包里常见的 native so 库报错、统信这类国产系统安装软件时提示“软件包架构不匹配”基本都是这个层面的问题和软件模块设计没有关系。第二类是安全度量比如 TPM 度量过程。TPM 里的“度量”指的是对固件、引导程序、内核做哈希记录用于可信启动和远程证明是一套完整的密码学校验流程。这个“度量”虽然也用测量和记录的思想但对象完全不同。第三类是机器学习里的等度量映射。搜索词里的“头歌降维-等度量映射”指的就是等度量映射这类流形学习方法它做的是计算样本之间的距离并降维同样不是软件架构问题。这些内容各有价值但都不是本文要展开的方向。我们要讨论的软件架构度量对象是代码模块和它们之间的关系。1.2 架构度量解决的真实痛点我见过很多系统最典型的状态是功能一直在加团队一直在换“架构变差”这件事所有人都知道但没有人能说清楚是从哪个版本开始变差的也不知道最严重的模块是哪几个。出现这种状态的原因很现实。架构是慢慢腐烂的。每加一个功能可能只是多了一个依赖每修一个 bug 可能只是在这个函数里多塞了一个分支。单看每一次提交都不至于让你停下来重构。但累积到一定量级就会出现改动一个基础模块牵连十几个模块重新编译、需要几十人一起评审的情况。架构度量解决的核心问题不是让架构变成满分而是让“变差”这件事可观测、可量化、可比对。有了度量可以回答三个非常实际的问题最近几次版本迭代架构是变好了还是变差了重构优先级应该排在哪里哪个模块最先动新人接手系统时哪些模块最容易踩坑这三个问题靠代码评审和口头感觉很难回答靠架构度量数据会清楚很多。2. 架构度量最需要看的指标以及怎么判断好坏度量指标有很多但不是每个都值得长期盯。我的经验是先盯结构、复杂度、可维护性这三类每类里选最核心的一两个指标比一口气上二十个指标有用得多。2.1 结构指标耦合、内聚、依赖方向结构指标回答的问题是模块之间边界清不清楚依赖方向对不对。耦合度是最常见的一类。看一个类或一个包被多少人引用、又引用了多少人也就是扇入和扇出。扇入很高的类通常是公共基础类但如果扇出也很高说明这个类什么都知道、什么都管是典型的上帝类特征。扇入扇出需要结合业务判断没有绝对的“多少算多”但一般单个类直接依赖超过二三十个其他类型时改动成本会明显上升。依赖方向比依赖数量更容易判断。检查依赖图里有没有跨层调用、有没有循环依赖。比如 controller 直接调用 repository、A 模块依赖 B 模块同时 B 又依赖 A这些都是典型的架构腐化信号。这类问题靠人肉看代码效率很低适合用静态依赖分析工具自动扫描。内聚度反映的是一个包或模块里的东西是不是真的围绕同一个职责。类级别的内聚可以用 LCOM 这类指标看方法之间共享字段的程度但更实用的做法是在包级别看一个包里的类是不是都被同一个业务任务调用还是被多个无关的业务场景各取一部分。前者是职责清晰后者往往是职责没划清楚包名是业务模块内容却是大杂烩。2.2 复杂度指标圈复杂度和认知复杂度复杂度指标回答的问题是代码改起来容不容易测试好不好写。圈复杂度是最经典的一个指标。简单理解它就是一段代码里独立路径的数量。if、for、while、switch、catch 每出现一个分支点复杂度就会增加。业界比较常见的参考是函数圈复杂度在 10 以下读起来比较轻松10 到 20 之间要留意测试场景会明显变多超过 20这个函数大概率已经承担了太多事情建议拆分。圈复杂度的缺点是没有考虑代码的排版和阅读顺序。两个嵌套的 if 和三个顺序排列的 if在圈复杂度上可能一样但认知负担完全不同。所以现在很多工具会叠加一个认知复杂度指标用来衡量“人读这段代码需要记住多少状态”。看代码质量时建议两个一起看圈复杂度高说明测试难写认知复杂度高说明人难读懂。2.3 模块级指标抽象度、稳定度、主序列距离类级别看完还要看包、组件这一层。评估一个大系统的模块结构时比较常用 Robert Martin 提出的组件级指标。稳定度 I 出向依赖数 /出向依赖数 入向依赖数。I 高说明这个组件更多依赖别人容易变化I 低说明被很多人依赖不宜随便改造。抽象度 A 组件里抽象类型数量 / 组件里类型总数。A 高说明组件偏接口化A 低说明偏具体实现。主序列距离 D |A I - 1|。D 越接近 0说明组件的抽象程度和稳定程度搭配得越合理。D 接近 1说明组件要么是“不稳定但实现很抽象”要么是“很稳定但实现全是具体类”至少有一头是别扭的。这里要特别说明这组指标适合组件和包级别不适合函数级别。而且“合理”不等于“所有组件都要 D 接近 0”。一个项目里总要有具体的、稳定不变的底层实现不能为了指标好看把每个底层包都抽象一层接口。2.4 可维护性指标与技术债务最后一类是综合指标。很多静态分析平台会计算一个可维护性指数通常是把圈复杂度、代码行数、代码体积、复杂度分布一起加权算出来的 0 到 100 的分数。常见参考可以这样记80 分以上维护性好60 到 80 分可以做适度优化60 分以下意味着每次改动都要小心翼翼。技术债务是另一个综合指标。它的本质是按当前代码的复杂度、重复度、坏味道估算出修复代价再和当前代码重写的成本做比值。技术债务率一般按“修复成本除以重写成本”来算常见工具会把它换算成需要多少个工作日。这个数字不精确但很有沟通价值因为管理层听得懂“还债需要多少个工作日”听不懂“圈复杂度环比上升了 5%”。把常用指标归个类方便按层级选指标回答的问题常见参考区间使用注意扇入/扇出单个类型被谁依赖、依赖谁无绝对标准关注异常高值要结合业务看公共基础类天然高循环依赖/跨层依赖模块边界是否失控0扫描后人工确认避免误报LCOM类内聚程度数值越高内聚越差类级别指标别用在组件层圈复杂度函数分支复杂度10 以下友好20 以上建议拆分参考值不同语言略有差异认知复杂度阅读者理解难度越低越好和圈复杂度配合使用主序列距离 D组件抽象与稳定度的匹配越接近 0 越合理组件级别不用于函数可维护性指数综合可维护程度80 以上好60 以下要关注不同平台算法有差异技术债务率修复坏味道的估计成本越低越好结合团队规模看是估算值适合沟通不适合考核3. 度量工具怎么选怎么接进已有工程指标定好之后下一步是找工具落地。工具不必一步到位从“能扫描依赖、能输出趋势”开始就足够了。3.1 从静态分析工具到架构规则市面上的静态分析平台比如 SonarQube 这类能覆盖圈复杂度、重复代码、可维护性指数、技术债务等大部分指标。它们通常提供网页端报表能看历史趋势和模块分布整体集成也比较成熟适合作为团队的基础度量平台。依赖关系这块通用静态分析覆盖得不够细。Java 项目可以用 ArchUnit 这类工具在单元测试里写架构规则比如“controller 包不能依赖 repository 包”“A 模块不能反向依赖 B 模块”跑测试的时候自动校验。其他语言也有类似的依赖分析库或脚本方案核心思路都是用语法树或字节码解析依赖输出依赖矩阵再和规则做比对。如果连工具平台都还没铺建议先别急着选重型方案。先在 IDE 里打开依赖分析功能把项目里最大那几个模块的依赖图导出来看一遍。很多时候最核心的问题不是工具没扫出来而是没人认真看过依赖图。3.2 把度量变成 CI 里的门槛度量最有价值的使用方式不是每个月跑一次给领导汇报而是变成开发流程里的自动门槛。具体做法分两步。第一步是建基线在当前版本上跑一遍全部指标把结果作为“存量问题”存档。第二步是设增量门槛新提交的代码如果引入新的跨层依赖、新增函数圈复杂度超过阈值或者模块扇出超过设定上限CI 直接失败。这个“存量 vs 增量”的区分非常关键。如果一开始就拿全量指标卡 CI一个历史工程大概率是满屏红灯团队要么直接关掉检查要么偷偷绕过最后门槛形同虚设。先让存量问题可见但不阻塞只拦增量问题团队才有动力逐步还债。3.3 度量需要准备的环境和数据度量不是拿到代码就能马上跑的。落地前至少要准备稳定的分析环境。代码分析工具依赖 JDK、构建工具、依赖仓库的版本。分析跑在 CI 容器里还是本地要尽量固定否则两次分析结果可能因为环境差异对不上。可对比的基线数据。只跑一次没有意义要把每次分析结果持久化至少保留最近几个月的趋势。模块划分的映射关系。尤其是老项目包的命名可能已经和业务模块对不上了。度量工具扫出来的“模块”默认是按包路径分的如果不对齐后面的报告会让你越看越疑惑。这些准备不复杂但不做的话度量报告很容易变成一串没有上下文的数字。4. 一套可落地的度量流程从定目标到看趋势有指标、有工具、有门槛还差一个流程把整个度量动作串起来。4.1 先定义“为什么度量”这个步骤很多人跳过但恰恰是最重要的。度量目标不同选的指标和阈值完全不同。如果目的是防止架构继续腐化重点看增量门槛新代码有没有绕过分层、有没有新增高复杂度函数。如果目的是决定重构优先级重点看存量分布哪个模块的复杂度最高、技术债务最重、被依赖方最多合并成一张“改动成本热力图”。如果目的是给管理层做技术状态同步那需要的是趋势数据和债务估算而不是一堆复杂度明细。我见过团队把架构度量做成了“每周跑一份全量报告发邮件”发了一个月没人看。原因就是目标不明确报告里的数据既不能指导重构也不能回答管理层的成本问题。4.2 建立基线和阈值目标定完开始建基线。先选一个稳定版本比如最近一个发版分支把核心指标全量跑一遍。记录下每个模块的耦合度、复杂度、依赖数、可维护性指数作为基线。基线不是拿来当目标的是拿来当参照的。以后每次分析对比的对象是这个基线而不是某个理想化的满分标准。阈值怎么定不要直接从网上抄一组数字就写进 CI。更稳妥的做法是先跑一周的数据看新代码的自然分布再把阈值设到“比当前最差那批再严格一点”的位置。比如存量函数圈复杂度最高的在 40新代码阈值先设 20跑一两个月能稳定通过再往下压到 15。这样团队不会一开始就抵触又能逐步收紧。4.3 确定度量频率和执行方式度量频率取决于你想拦截什么问题。单个 PR 级在 pull request 或 merge request 的 CI 阶段跑增量分析只查本次改动有没有引入新的架构违规。这是性价比最高的一道门槛。夜间/每日级跑全量静态分析和依赖扫描输出趋势数据。这个适合盯整体健康度发现“最近三天新增了多少坏味道”。发版级在 release 分支再做一次完整度量生成版本间的技术债务对比报告。这个适合向管理层同步或者复盘本次发版是否有架构层面的意外变更。三种频率不是必须全上。小团队先做 PR 级和发版级就够夜间全量可以等 CI 资源充裕再加。4.4 最小流程示例如果你从零开始第一步可以只做这样一件事选一个核心服务跑一次依赖分析输出模块清单和依赖关系然后把结果存进仓库的 docs 目录。对比是后面的事先把“看到全貌”这件事完成。一个最小的度量流程大致是这个样子# 示例先拉代码再跑分析最后对比基线 git checkout main git pull # 执行静态分析不同工具命令不同这里只示意流程 # 导出圈复杂度、依赖矩阵、技术债务等指标 # 与上一次结果对比 # 如果 增量违规数 0 或 核心模块复杂度上升超过阈值则 CI 失败 # 失败时通知团队并附上分析报告链接实际在 CI 里配置时分析步骤前通常还要先安装依赖、缓存构建产物这些和具体技术栈强相关不展开。核心就一句话把“跑分析、对基线、判结果”三步固定成一个自动化任务不要依赖人工点击。5. 数字好看不等于架构健康解读指标要避开的坑度量指标用习惯了容易产生一种错觉所有数字都在安全范围内说明架构没问题。这个判断很危险。5.1 指标是代理变量不是最终目标复杂度低不等于好设计。把一个大函数拆成十几个小函数每个函数都只有五行业务代码圈复杂度确实降下来了但如果这些小函数之间通过大量成员变量和参数传递耦合在一起整体可读性可能更差。耦合低也不一定等于好。过度拆分模块把本来一个事务里需要一致更新的东西拆成多个服务每个服务内部耦合都很低但调用链和分布式一致性复杂度会飙升。这个复杂度根本不体现在代码静态指标里。所以读度量报告时要养成一个习惯每个数字下降或上升都要先问一句“这是不是被某种写法刷出来的”。5.2 常见的“刷分数”行为我总结过几类很典型的刷分行为出现任何一种都说明团队已经把指标当成了考核项而不是质量信号。第一类是把函数拆小但没拆职责。圈复杂度从 30 降到 5但代码流程还是晦涩只是把一个判断挪到了另一个函数里。第二类是加无意义的抽象层来改善依赖方向。为了让组件稳定度 I 变小给根本不会多态使用的类强行加接口。第三类是优先修简单的坏味道把技术债务率压下去但真正有风险的模块一点没动。第四类是发现分析不过直接删除或排除代码跳过上报而不是实际修复。这些都是实际项目里会出现的做法而且不一定是团队故意造假很多时候是考核压力下的自然反应。要避免它最好的办法是度量数据用于指导不用于考核。5.3 不同模块要分开看不能一刀切基础设施模块和业务模块复杂度的合理性完全不一样。一个缓存组件、一个 RPC 框架代码天然比 CRUD 业务更抽象抽象度 A 高是正常的。一个订单状态机分支多、圈复杂度高可能也是合理的因为订单流程本身就是一堆状态和异常路径。直接把所有模块按同一个阈值卡结果就是把不同性质的代码往同一个标准上赶。更合理的做法是先给模块打标签按“基础设施、核心业务、边缘业务、历史遗留”分开统计分别定参考范围。这也是为什么我不建议团队之间横向比较架构度量数字。两个团队业务复杂度和历史包袱不一样A 团队的圈复杂度中位数比 B 团队高不代表 A 团队代码更差可能只是 A 做的是支付核心B 做的是展示页面。6. 度量落地后的常见问题与排查顺序度量系统本身也是系统也会出问题。这里列几个最常见的现象和排查思路。6.1 同一个项目两次度量结果不一样这个现象很常见尤其容易出现在刚接 CI 的时候。先查分析环境。依赖版本变了、JDK 版本变了、分析工具升级了都会导致结果漂移。解决办法是让 CI 里的分析环境固定锁好工具版本和依赖缓存。再查分析范围。增量分析只扫改动文件和全量分析扫所有文件出来的模块复杂度本来就不该一样。这里最容易踩的坑是拿增量分析的结果和全量基线做对比发现复杂度大幅波动其实是统计口径不一致。如果这些都没问题再查是不是最近有大规模重构或依赖升级。这种波动是真实的反而值得关注。6.2 指标突然变差先查哪里假设你发现核心模块的耦合度或者技术债务率一夜之间涨了一截不要急着改代码按这个顺序排查。先看版本。是不是基线分支换了或者分析跑到了某个未经合并的分支上基线错位会导致全量指标变化。再看依赖变更。最近有没有升级框架版本、引入新的第三方库可能只是多了一个传递依赖就导致模块依赖数暴涨。然后看批量改动。有没有人做过一次大范围的自动格式化、重命名、包结构调整这类提交容易在依赖图上产生大量噪音。最后再看存量统计口径。如果是全量指标的涨幅要确认分析工具是不是把新增的自动生成代码或者第三方源码也算进去了。排查的核心原则是先排除统计口径和环境问题再判断是不是真实架构变化。6.3 从“先跑起来”到“长期维护”的优先级建议最后给一个优先级建议。第一优先级先把一个核心模块的依赖图看明白人工确认三个问题有没有循环依赖、有没有跨层调用、最底层被普遍依赖的模块是不是稳定。这三条确认完再配工具。第二优先级选一套能持续出历史趋势的分析平台哪怕只用它的三个核心指标也要保证数据持久化和定期报告。第三优先级在 CI 里加入增量架构检查设置一个偏宽松的初始阈值。第四优先级才是逐步铺开全量覆盖、多种指标、多团队对比这些复杂玩法。我见过很多团队跳过前两步直接上了一整套可视化大屏最后大屏没人看数据也是第一次导入后就没人维护。架构度量真正有价值的形态不是一张越来越花的报表而是一套能回答关键问题的基础数据。把这套基础数据维护好比追求指标丰富度重要得多。
返回列表