本文属于 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. Hive 自定义UDF操作步骤

    Hive 自定义UDF操作步骤 需要自定义类,然后继承UDF 然后在方法envluate()方法里面实现具体的业务逻辑,打包上传到linux(以免出错打包成RunningJar) 一.创建临时函数 ( ...

  2. misc办公室爱情

    ​ 隐藏文字password2 ​编辑 word改后缀zip解开后document.xml找到password1 ​编辑 True_lOve_i2_supReMe 用wbs43open+密码解密pdf ...

  3. iOS- 最全的真机测试教程

      想要上架的同学请看:<iOS-最全的App上架教程> 因为最近更新了Xcode 8 ,证书的创建都大同小异,只是在Xcode 8中的设置有一些变化,我就在下面补充,如有什么疑问,请联系 ...

  4. docker搭建ddns

    ddns 容器 https://hub.docker.com/r/chen... https://github.com/honwen/ali... docker pull chenhw2/aliyun ...

  5. 云原生之旅 - 10)手把手教你安装 Jenkins on Kubernetes

    前言 谈到持续集成工具就离不开众所周知的Jenkins,本文带你了解如何在 Kubernetes 上安装 Jenkins,后续文章会带你深入了解如何使用k8s pod 作为 Jenkins的build ...

  6. gorm

    特性 全功能 ORM 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承) Create,Save,Update,Delete,Find 中钩子方 ...

  7. Devexpress 图表显示数据标签

    dev的图标功能非常强大其中有一些设置可以更好的展现出数据 设置Series的标签 series.LabelsVisibility = DevExpress.Utils.DefaultBoolean. ...

  8. 北极星Polaris+Gateway动态网关配置!

    springcloudtencetn 父工程: pom <?xml version="1.0" encoding="UTF-8"?> <pro ...

  9. OSError: dlopen() failed to load a library: cairo / cairo-2 / cairo-gobject-2 / cairo.so.2

    解决办法 下载 gtk3-runtime-3.24.29-2021-04-29-ts-win64.exe后安装. 记得勾选添加bin目录到环境变量: 这样就不会缺失dll了,当然可能需要重启IDE才能 ...

  10. uniCloud云开发入门以及对传统开发方式的思考

    事情缘由 作为选修了移动互联网应用的一员,老师讲的什么JS基础,还有ES6和uniapp,当然是没怎么听,因为是之前大二的时候都大概看过. 但是快到期末,老师讲了云开发,并且布置了与此相关的大作业,自 ...