代码的圈复杂度

2025-11-09T13:22:19+08:00 | 3分钟阅读 | 更新于 2025-11-09T13:22:19+08:00

@
代码的圈复杂度

圈复杂度是一种衡量代码复杂度的指标,它可以帮助我们识别出代码中的复杂区域,从而更好地优化代码。圈复杂度越高,代码的复杂度越高,也就越难以阅读和维护。

概念起源

这个概念由美国计算机科学家 Thomas J. McCabe, Sr. 在 1976 年的论文《A Complexity Measure》中首次提出,因此也被称为 McCabe 复杂度

在 20 世纪 70 年代,随着软件系统规模急剧膨胀,测试 and 维护成本变得极高。当时的开发者缺乏一种客观、定量的手段来评估代码的“复杂度”。

McCabe 引入了图论(Graph Theory)的思想,将程序执行过程抽象为控制流图(Control Flow Graph)。其初衷是提供一个严谨的指标,用于:

  • 识别高风险代码:定位逻辑嵌套过深、易隐藏 Bug 的模块。
  • 指导测试用例设计:圈复杂度直接对应实现“分支覆盖”(Branch Coverage)所需的最小测试用例数。

McCabe 建议,单个模块或函数的圈复杂度不应超过 10,否则应考虑重构或拆分。

计算公式

基于图论,将代码转化为控制流图后,**节点(Nodes)**代表顺序执行的代码块,**边(Edges)**代表控制流转(如 ifwhile)。

计算公式为:

$$ M = E - N + 2P $$

  • $M$:圈复杂度
  • $E$:控制流图中边的数量
  • $N$:控制流图中节点的数量
  • $P$:连接组件的数量(对于单个独立的程序或函数,通常 $P=1$)

此时公式简化为:

$$ M = E - N + 2 $$

案例分析

通过具体代码来理解最为直观。我们看一个典型的 Python 业务方法:根据客户类型和购买金额计算折扣。

1. 代码示例

def calculate_discount(customer_type, purchase_amount):
    discount = 0.0                      # N1
    
    if customer_type == "VIP":          # N2
        if purchase_amount > 1000:      # N3
            discount = 0.20             # N4
        else:                           # N5
            discount = 0.10
    elif customer_type == "Regular":    # N6
        if purchase_amount > 500:       # N7
            discount = 0.05             # N8
            
    return purchase_amount * (1 - discount) # N9

2. 节点划分

在这个方法中,我们可以划分出 9 个节点

  • N1: 初始化 discount = 0.0
  • N2: 判断 customer_type == "VIP"
  • N3: 判断 purchase_amount > 1000
  • N4: 赋值 discount = 0.20
  • N5: 赋值 discount = 0.10(隐式 else)
  • N6: 判断 customer_type == "Regular"
  • N7: 判断 purchase_amount > 500
  • N8: 赋值 discount = 0.05
  • N9: 执行 return

3. 控制流图

  graph TD
    N1["N1: 初始化 discount"] -->|E1| N2{"N2: VIP?"}
    
    N2 -->|E2: Yes| N3{"N3: Amount > 1000?"}
    N2 -->|E3: No| N6{"N6: Regular?"}
    
    N3 -->|E4: Yes| N4["N4: 0.20"]
    N3 -->|E5: No| N5["N5: 0.10"]
    
    N4 -->|E6| N9(["N9: Return"])
    N5 -->|E7| N9
    
    N6 -->|E8: Yes| N7{"N7: Amount > 500?"}
    N6 -->|E9: No| N9
    
    N7 -->|E10: Yes| N8["N8: 0.05"]
    N7 -->|E11: No| N9
    
    N8 -->|E12| N9
    
    classDef condition fill:#f9f0ff,stroke:#b266ff,stroke-width:2px;
    class N2,N3,N6,N7 condition;

4. 计算过程

梳理出 12 条边(执行路径)

  • E1: N1 -> N2 (顺序)
  • E2: N2 -> N3 (VIP 是)
  • E3: N2 -> N6 (VIP 否)
  • E4: N3 -> N4 (>1000)
  • E5: N3 -> N5 (<=1000)
  • E6: N4 -> N9
  • E7: N5 -> N9
  • E8: N6 -> N7 (Regular 是)
  • E9: N6 -> N9 (Regular 否)
  • E10: N7 -> N8 (>500)
  • E11: N7 -> N9 (<=500)
  • E12: N8 -> N9

套用公式 $M = E - N + 2$:

$$ M = 12 - 9 + 2 = 5 $$

结论:该方法的圈复杂度为 5。这意味着要实现 100% 的分支覆盖,至少需要编写 5 个测试用例

自动化测量工具

在真实的工程中,手动计算并不现实。对于 Python 项目,我们通常使用 radon 库来自动计算。

1. 安装与使用

通过 pip 安装:

pip install radon

假设将上述代码保存为 calc.py,在终端运行:

radon cc calc.py -a

2. 输出解读

终端会输出类似结果:

calc.py
    F 1:0 calculate_discount - A (5)

1 blocks (classes, functions, methods) analyzed.
Average complexity: A (5.0)
  • 5:计算出的圈复杂度。
  • A:代码健康评级(A 最好,F 最差)。
    • A (1-5):非常健康,逻辑简单。
    • B (6-10):结构良好,复杂度可控。
    • C (11-20):稍显复杂,建议关注。

小结

我平时用的比较深也只有单元测试的行覆盖率指标了,理论上跟圈复杂度应该有正相关的关系,但是没 有深入研究过。

代码圈复杂度有个优点是可以明确的告诉你,要实现100%的行覆盖率,需要写多少个测试用例, 这对于像我们研发写测试用例的开发流程来说,可以更好的评估工作量、也可以防止测试用例写不全的 情况。

© 2026 火箭的博客

🌱 Powered by Hugo with theme Dream.

爱好
  • 三国演义:从大二开始听评书版的《三国演义》,现在主要是作为睡前小故事来听(我估计是第七八遍了)
  • 写代码:没错,写代码也是我的爱好。只要出门超过半天,我基本都会背着笔记本,随时可能进入“编码模式”。
  • 健身:曾经的爱好。结婚之后就逐渐被抛弃了 🤷,结婚一年涨了10斤。
  • 看电影:后续可能会写影评。
  • 探索新事物和工具:准备开一个主题,专门分享我接触到的各种新玩意儿。
关于我

👋 你好,我是 huojian (火箭)

欢迎来到我的个人博客!建立这个小站的初衷,是想在浩瀚的互联网中拥有一块属于自己的“数字花园”。我经常从各类优秀的博客中汲取养分,现在,我也想成为那个输出和分享的人。

这里没有刻板的文章更新 KPI,更多的是我个人的技术沉淀、学习笔记以及生活碎片的真实记录。

👨‍💻 关于我与我的技术栈

我是一名客户端软件工程师,日常与代码和系统架构打交道。在技术探索的道路上,我享受解决复杂问题带来的成就感,也喜欢折腾各种能提升幸福感的工具。

在这里,你可能会看到我分享以下内容:

  • 💻 开发与架构探讨: 探讨 WPF/C# 领域的开发经验、代码质量优化,以及诸如客户端热修复 (Hotfix) 系统设计等我在实际项目中遇到并解决的挑战。
  • 🛠️ 工作流与跨平台折腾: 记录我游走在 Windows 与 macOS 环境下的效率心得。从外设配置到好用的跨平台开发工具,分享如何打造顺手的生产力环境。
  • 🐳 自托管与数字生活: 我是个“自建服务”爱好者。会不定期掉落关于 Docker 部署、云服务器折腾指南,以及如何搭建属于自己的 RSS 阅读环境等教程。
  • 📝 学习笔记与自我精进: 记录各种技术学习心得。同时,我也在持续死磕英语,努力提升口语和写作能力,告别“哑巴英语”,相关的学习路径和思考也会记录在这里。

💡 我的理念

“Talent is enduring patience.” (才能即是长久的忍耐)

我相信持续输出的力量。无论是深度的技术解析,还是一个简单的开发踩坑记录,亦或是一篇普通的生活随笔,都是打造个人 IP 与技术影响力的基石。

📬 建立连接

非常高兴能在这里与同样喜欢写代码、喜欢阅读的你相遇。如果你对我的文章有共鸣,或者想交流技术与生活,欢迎随时找我:

  • GitHub: @huojian-jan
  • RSS 订阅: 欢迎通过博客右上角的 RSS 订阅我的最新文章。
记录什么?
  • 学习笔记

  • 一些想法:记录日常思考,不一定有结论,更像是一种随手的表达。

  • 旅游与随笔:旅行时的见闻与感受,以及偶尔的文字随笔。