本文属于 dotnet 代码优化系列博客。相信大家都对圈复杂度这个概念很是熟悉,本文来和大家聊聊逻辑的圈复杂度。代码优化里面,一个关注的重点在于代码的逻辑复杂度。一段代码的逻辑复杂度越高,那么维护起来的难度也就越大。衡量代码的逻辑复杂度的一个维度是通过逻辑圈复杂度进行衡量。本文将告诉大家如何判断代码的逻辑圈复杂度以及一些降低圈复杂度的套路,让大家了解如何写出更好维护的代码

回顾一下代码设计的目标,其中一个很重要的点就是解决 复杂的代码逻辑 和 人类有限的智商 的矛盾。假设人类的智商非常的高,无论再复杂的代码逻辑都能理解,且人类写出的逻辑也不存在漏洞,那其实很多代码设计都是不需要的。现实刚好不是,一个稍微复杂的项目,就已经不是人类轻而易举能够掌控的。即使是自己编写的代码,也会随着时间逐渐遗忘代码里面当初的实现逻辑。何况在团队协作中,可能会遇到需要阅读其他开发者留下的代码的时候,假设前辈们没有好好的进行编写和设计,自然可能是给后来者挖了一个大坑

逻辑的圈复杂度属于一个度量代码复杂度的维度,但稍微特别的是,当逻辑的圈复杂度比较低时,能意味着代码复杂度比较低,比较好维护。但反过来不成立,比较好维护的代码,不一定是逻辑的圈复杂度比较低的代码。代码的可维护是需要综合考虑多个维度的,虽然说降低逻辑的圈复杂度基本上都是属于正确的事情,但由于实际项目遇到的情况比较特殊,还请识别主次矛盾,不要强行优化

逻辑的圈复杂度是指在代码执行过程中,逻辑上形成的圈的数量,更多的是指在面向对象设计里面的类和方法之间的关系。至于方法内的循环判断等,只属于(代码)圈复杂度(Cyclomatic complexity)而不是逻辑圈复杂度

学术的定义,相信大家都不感兴趣,下面来举一个例子,相信大家看完很快就懂了

例子依然是老套的图书管理系统的故事,假定书籍有 人文、哲学、物理、数学、计算机 等类型的书籍,在图书管理系统里面,需要有一定的业务逻辑,对其进行处理。其工序有些是所有类型共用的,有些是需要根据类型而来的,假定每个工序都能用一个代码方法完成。原始的逻辑设计抽象起来如下图

从逻辑上看,以上的逻辑设计是存在很多个圈圈的,相当于不停的拆分、聚合,每一次都是在增加逻辑圈复杂度,这样的逻辑设计对应到代码里面,大概就是一堆 if 或者 switch 判断,控制其后续走向,或者是面向对象的继承关系,让调用穿插在基类和子类之间。假定以上的逻辑设计属于使用了 一堆 if 或者 switch 判断的方式,那自然在区分输入类型和工序1里面,都会存在判断书籍类型,以调用后续逻辑的代码,伪代码如下

void 区分输入类型()
{
if (书籍类型 == 人文)
{
人文_工序0();
}
else if (书籍类型 == 哲学)
{
哲学_工序0();
}
else if(...)
{
...
}
} void 人文_工序0()
{
// 工序的逻辑
... 不分图书类型的_工序1();
} void 哲学_工序0()
{
// 工序的逻辑
... 不分图书类型的_工序1();
} void 不分图书类型的_工序1()
{
// 工序的逻辑
... if (书籍类型 == 人文)
{
人文_工序2();
}
else if (书籍类型 == 哲学)
{
哲学_工序2();
}
else if(...)
{
...
}
} ...

从以上的伪代码也可以看到,在 区分输入类型不分图书类型的_工序1 之间,存在逻辑比较相似的代码,那就是拆分书籍类型,然后调用不同的方法。当书籍的类型足够多的时候,这个逻辑维护起来就开始令人烦躁起来了,当工序同样多起来的时候,那就更加不好玩咯

来数数逻辑的圈圈数量,猜猜有多少个圈圈?如下图标记出来的只有 4 个圈圈对不

其实没有那么简单。嗯,不严谨的算,上面的逻辑设计图至少有 9 个圈圈

如果列出更多的书籍类型,以及更多的工序,那这个圈的数量能够更加庞大

大家也可以想想看,每加一个书籍类型,会加多少个圈圈?世界上还有一群专家也在研究加一个模块或一个功能时,圈复杂度的增加速率。在某些时候的设计上,会导致加一个模块或加一个功能时,增加的圈圈数量会越来越多。例如上面的逻辑设计图在两个书籍类型,也就是两个模块时,只有三个圈圈,但是在有三个模块时,就有 9 个圈圈了。也可以看到,随着书籍类型的数量,也就是模块的数量,不断增加的时候,每加一个时,增加的圈圈数量会越来越多,这也就表示了逻辑复杂度每次增加都会越来越多

换一句话说,如果按照上面的逻辑设计图的方式进行开发,会发现越开发越复杂。即使开发者有着很好的编写代码的能力,也会逐渐发现整个项目越来越难以掌控。在设计上存在将会导致必然出现的代码逻辑圈复杂度时,会导致项目在开发过程中是上帝和程序猿才能看懂代码,开发一定时间之后,就只有上帝才能看懂代码了

在了解基础的知识之后,大家也许会问,那如何改造降低圈复杂度呢?一个套路方法就是在区分类型之后,让数据的走向被具体类型进行控制,这也是面向对象里,多态的一个用法。具体来做就是在 区分输入类型 的类型之后,进入某个类型的书籍的总处理方法,在某个类型的总处理方法里面,可以愉快的从工序的开始执行到工序的结束

再来数一下逻辑的圈复杂度,是不是一个圈也数不到了?对应的代码大概如下,可以看到每个总工序里面处理的逻辑一目了然

void 人文_总工序()
{
人文_工序0();
工序1();
人文_工序2();
工序3();
} void 哲学_总工序()
{
哲学_工序0();
工序1();
哲学_工序2();
工序3();
}

啥都不用说,对比代码量就知道,看代码的清晰程度也能看起来降低圈复杂度之后的优化

那这时,也许有伙伴说,如果各个总工序都十分相似,是不是也可以再抽一下?是的,但是也需要看情况,如果少部分的重复逻辑可以带来更多的代码清晰度,那这部分的逻辑留着也是可以接受的。但如果在抽一下基础类型之后,发现逻辑依然清晰,那就开干吧,毕竟重复的逻辑也不是什么好的事情

定义一个书籍处理的抽象基类,然后在此基类里面放总工序,接着各个具体的书籍处理类型,继承基类,编写实现方法,伪代码如下

abstract class 书籍管理基类
{
public void 总工序()
{
工序0();
工序1();
工序2();
工序3();
} protected abstract void 工序0(); private void 工序1()
{
// ...
}
protected abstract void 工序2(); private void 工序3()
{
// ...
}
} class 人文书籍管理 : 书籍管理基类
{
protected override void 工序0()
{
人文_工序0();
} private void 人文_工序0()
{
// ...
} protected override void 工序2()
{
人文_工序2();
} private void 人文_工序2()
{
// ...
}
} class 哲学书籍管理 : 书籍管理基类
{
protected override void 工序0()
{
哲学_工序0();
} private void 哲学_工序0()
{
// ...
} protected override void 工序2()
{
哲学_工序2();
} private void 哲学_工序2()
{
// ...
}
}

可以看到,这大概也就是一个超级简单的框架了,具备了一定的扩展性,也就是后续如果还需要加上新的书籍类型,也是非常方便的,只需要定义多一个类型即可,同时逻辑上也相对来说比较清真,没有那么复杂

以上是借助 C# 里面的抽象类实现的,这个套路需要不断让子类型进行重写方法,导致逻辑上可能部分是在基类,部分是在子类。不过以上的代码写法是没有问题的,因为继承关系才只有两层,但如果继承关系更多了呢?假设有三层甚至更高呢?这时执行逻辑可能需要跨越多个类型,那逻辑复杂度也会上来

假定有如下图的逻辑,需要按照顺序或者是执行时间,分别调用方法1到6来完成业务端的任务。当存在让子类型层层继承的基类有三个的时候,如果调用方法散落在这个基类里面,那逻辑复杂度将会是非常高的,很多时候静态阅读代码都非常有难度

如上图,假设以上没有画出来图,而是写成代码,那想要静态阅读代码,了解其中的执行逻辑,预计看了一会开始乱了,不知道对应的方法应该在哪个类型里面,哪个文件里面。好在 C# 里面禁用了多类型继承,否则能写出连示意图画出来都能劝退人的代码。可是 C# 里面也有一个叫虚方法的定义,允许在基类里面定义虚方法,看子类的心情去进行重写,有重写就使用子类的,没重写就采用基类的,上图里面的方法 6 是一个虚方法,在基类 2 里面定义,但是在 基类 3 被重写。这时将会发现静态阅读的代码,不见得就是实际运行的代码。例如阅读到基类 2 里面定义了方法 6 的逻辑,然而实际运行的时候,执行的是基类 3 的逻辑

这里需要补充一点的是静态阅读代码指的是和调试阅读代码相对的阅读代码方式,指的是在不开始进行调试的方式进行阅读代码,可以在 IDE 的辅助下,例如在 VisualStudio 这样的 IDE 辅助下阅读代码。好维护的代码是需要考虑静态阅读代码的,因为很多时候调试的时候能跑的路径不会特别全,也不会特别多,甚至有些逻辑是存在很多前置条件的,仅靠调试来了解执行方式,可能了解到不全面

这也是某些开发老司机会说的“组合优于继承”的其中一点原因,大量的继承将会导致逻辑散落在各地,不够“内聚”导致逻辑复杂度上升。值得一提是 “组合优于继承” 这句话是具备大量前提的,还请不要将这句话作为开发的规范

那什么时候应该选择什么方法?其实十分主观,我的推荐是多试试看,写多了,然后将自己坑多了,自然就知道了。主动去看自己之前写过的复杂逻辑(最好别去看别人的,否则心态可能会炸)看看是否会感觉自己无法理解逻辑,如果会的话,再想想可以使用什么方式,如果再写一次的话,可以更加方便阅读代码理清逻辑

回顾一下,本文告诉了大家什么是代码逻辑圈复杂度,以及降低逻辑圈复杂度的套路方法。同时也告诉了大家,这个套路也不是万能的,做的不好也可以提升代码复杂度

更多代码编写相关博客,请参阅我的 博客导航

特别感谢 小方 帮忙改正

dotnet 代码优化 聊聊逻辑圈复杂度的更多相关文章

  1. C语言switch/case圈复杂度优化重构

    软件重构是改善代码可读性.可扩展性.可维护性等目的的常见技术手段.圈复杂度作为一项软件质量度量指标,能从一定程度上反映这些内部质量需求(当然并不是全部),所以圈复杂度往往被很多项目采用作为软件质量的度 ...

  2. 圈复杂度(Cyclomatic Complexity)

    圈复杂度(Cyclomatic Complexity)是很常用的一种度量软件代码复杂程度的标准.这里所指的“代码复杂程度”并非软件内在业务逻辑的复杂程度,而是指代码的实现方式的 复杂程度.说起来有点绕 ...

  3. windows+goland+gometalinter进行本地代码检查(高圈复杂度、重复代码等)

    1.下载gometalinter release地址为:https://github.com/alecthomas/gometalinter/releases/tag/v3.0.0 下载windows ...

  4. 什么是Cyclomatic Complexity(圈复杂度)?

    Campwood Software SourceMonitor Version 3.5 The freeware program SourceMonitor lets you see inside y ...

  5. [代码质量] 代码质量管控 -- 复杂度检测 (JavaScript)

    转载自: https://juejin.im/post/59bb8b546fb9a00a4247532e 背景 代码的复杂度是评估一个项目的重要标准之一.较低的复杂度既能减少项目的维护成本,又能避免一 ...

  6. 怎样避免 i f 判断过多,全复杂度较高,代码不美观的问题?

    没有什么好的设计方式可以实现,减少一个方法中出现几十个 if 匹配的判断? 现在要做一个判断客户是否通过验证的接口. 一共有30多个验证规则的判断, 每个规则对应一个规则号: 这个接口只需要返回是否验 ...

  7. 通过 Visual Studio 的“代码度量值”来改进代码质量

    1 软件度量值指标 1.1 可维护性指数 表示源代码的可维护性,数值越高可维护性越好.该值介于0到100之间.绿色评级在20到100之间,表明该代码具有高度的可维护性:黄色评级在10到19之间,表示该 ...

  8. 通过Visual Studio 的“代码度量值”来改进代码质量

    1 软件度量值指标 1.1 可维护性指数 表示源代码的可维护性,数值越高可维护性越好.该值介于0到100之间.绿色评级在20到100之间,表明该代码具有高度的可维护性:黄色评级在10到19之间,表示该 ...

  9. 如何计算并测量ABAP及Java代码的环复杂度Cyclomatic complexity

    代码的环复杂度(Cyclomatic complexity,有的地方又翻译成圈复杂度)是一种代码复杂度的衡量标准,在1976年由Thomas J. McCabe, Sr. 提出. 在软件测试的概念里, ...

  10. [代码质量] 推荐一个vs自带工具分析代码的复杂度

    转载自: https://blog.csdn.net/zh_geo/article/details/52954145 VS2012 -> Analyze -> Calculate code ...

随机推荐

  1. Dubbo2.7详解

    Spring与Dubbo整合原理与源码分析 [1]注解@EnableDubbo @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTI ...

  2. LeetCode------斐波那契数列(2)

    来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/fei-bo-na-qi-shu-lie-lcof 写一个函数,输入 n ,求斐波那契(Fibo ...

  3. django 生产环境部署手册

    Django 是 python 的 web 框架,以下是其部署到生产环境的详细步骤,包含 Apache 和 nginx 版本. 部署环境 操作系统:centeros7.3 数据库:MySQL5.6.5 ...

  4. 《吐血整理》高级系列教程-吃透Fiddler抓包教程(31)-Fiddler如何抓取Android系统中Flutter应用程序的包

    1.简介 Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面.Flutter应用程序是用Dart编写的,这是一种由Google在7年多前创建的语言.Flut ...

  5. 深度优先搜索(Depth-First-Search)dfs代码模板

    void dfs()//参数用来表示状态 { if(到达终点状态) { ...//根据需求添加 return; } if(越界或者是不合法状态) return; if(特殊状态)//剪枝,去除一些不需 ...

  6. C# 语法分析器(二)LR(0) 语法分析

    系列导航 (一)语法分析介绍 (二)LR(0) 语法分析 (三)LALR 语法分析 (四)二义性文法 (五)错误恢复 (六)构造语法分析器 首先,需要介绍下 LALR 语法分析的基础:LR(0) 语法 ...

  7. [VUE]报错: No Babel config file detected for

    在使用vue脚手架创建的项目中,项目中每个文件的第一行都会有红色波浪线. 解决方法:在项目文件中找到package.json文件,在parserOptions里添加"requireConfi ...

  8. Python基础部分:2、 对计算机的认识和python解释器

    目录 一.计算机五大组成部分 1.控制器 2.运算器 3.储存器 4.输入设备 5.输出设备 二.计算机三大核心硬件 1.cpu 2.内存 3.硬盘 三.操作系统 四.编程与编程语言 1.编程语言 2 ...

  9. 一个超经典 WinForm 卡死问题的再反思

    一:背景 1.讲故事 这篇文章起源于昨天的一位朋友发给我的dump文件,说它的程序出现了卡死,看了下程序的主线程栈,居然又碰到了 OnUserPreferenceChanged 导致的挂死问题,真的是 ...

  10. 关于图计算&图学习的基础知识概览:前置知识点学习(Paddle Graph Learning (PGL))

    关于图计算&图学习的基础知识概览:前置知识点学习(Paddle Graph Learning (PGL)) 欢迎fork本项目原始链接:关于图计算&图学习的基础知识概览:前置知识点学习 ...