为什么开发软件装起来那么复杂,开发一个软件有那么容易吗

首页 > 实用技巧 > 作者:YD1662023-04-22 16:22:53

作者:王洋 阿里ICBU技术团队

本文将重点围绕软件复杂度进行剖析,希望能够帮助读者对软件复杂度成因和度量方式有所了解,同时,结合自身的实践经验谈谈我们在实际的开发工作中如何尽力避免软件复杂性问题。

大型系统的本质问题是复杂性问题。互联网软件,是典型的大型系统,数百个甚至更多的微服务相互调用/依赖,组成一个组件数量大、行为复杂、时刻在变动(发布、配置变更)当中的动态的、复杂的系统。而且,软件工程师们常常自嘲,“when things work, nobody knows why”。

一、导致软件复杂度的原因

导致软件复杂度的原因是多种多样的。

宏观层面讲,软件复杂是伴随着需求的不断迭代日积月累的必然产物,主要原因可能是:

对于前三点我觉得可以通过日常的工程师文化建设来尽量避免,但是随着业务的不断演化以及人员的流动、知识传递的缺失,长期的叠加之下必然会使得系统越发的复杂。此时,我觉得还需要进行系统的重构。

从软件开发微观层面讲,导致软件复杂的原因概括起来主要是两个:依赖(dependencies) 和 隐晦(obscurity)。

依赖会使得修改过程牵一发而动全身,当你修改模块一的时候,也会牵扯到模块二、模块三等等的修改,进而容易导致系统bug。而隐晦会让系统难于维护和理解,甚至于在出现问题时难于定位问题的根因,要花费大量的时间在理解和阅读历史代码上面。

软件的复杂性往往伴随着如下几种表现形式。

1.1 修改扩散

修改时有连锁反应,通常是因为模块之间耦合过重,相互依赖太多导致的。比如,在我们认证系统中曾经有一个判断权益的接口,在系统中被引用的到处都是,这种情况会导致一个严重问题,今年这个接口正好面临升级,如果当时没有抽取到一个适配器中去,那整个系统会有很多地方面临修改扩散的问题,而这样的变更比较抽取到适配器的修改成本是更高更风险的。

@Override public boolean isAllowed(Long accountId, Long personId, String featureName) { boolean isPrivilegeCheckedPass = privilegeCheckService.isAllowed( accountId, personId, featureName); return isPrivilegeCheckedPass; }1.2 认知负担

当我们说一个模块隐晦、难以理解时,它就有过重的认知负担,开发人员需要较长的时间来理解功能模块。比如,提供一个没有注释的计算接口,传入两个整数得到一个计算结果。从函数本身我们很难判断这个接口是什么功能,所以此时就不得不去阅读内部的实现以理解其接口的功能。

int calculate(int v1, int v2);1.3 不可知(Unknown Unknowns)

相比于前两种症状,不可知危险更大,在开发需求时,不可知的改动点往往是导致严重问题的主要原因,常常是因为一些隐晦的依赖导致的,在开发完一个需求之后感觉心里很没谱,隐约觉得自己的代码哪里有问题,但又不清楚问题在哪,只能祈祷在测试阶段能够暴露出来。

二、软件复杂度度量

Manny Lehman教授在软件演进法则中首次系统性提出了软件复杂度:

软件(程序)复杂度是软件的一组特征,它由软件内部的相互关联引起。随着软件的实体(模块)的增加,软件内部的相互关联会指数式增长,直至无法被全部掌握和理解。

软件的高复杂度,会导致在修改软件时引入非主观意图的变更的概率上升,最终在做变更的时候更容易引入缺陷。在更极端的情况下,软件复杂到几乎无法修改。

在软件的演化过程中,不断涌现了诸多理论用于对软件复杂度进行度量,比如,Halstead复杂度、圈复杂度、John Ousterhout复杂度等等。

2.1 Halstead 复杂度

Halstead 复杂度(霍尔斯特德复杂度量测) (Maurice H. Halstead, 1977) 是软件科学提出的第一个计算机软件的分析“定律”,用以确定计算机软件开发中的一些定量规律。Halstead 复杂度根据程序中语句行的操作符和操作数的数量计算程序复杂性。针对特定的演算法,首先需计算以下的数值:

上述的运算子包括传统的运算子及保留字,运算元包括变数及常数。依上述数值,可以计算以下的量测量:

为什么开发软件装起来那么复杂,开发一个软件有那么容易吗(1)

举一个例子,这是一段我们当前应用中接入AB实验的适配代码:

try { DiversionRequest diversionRequest = new DiversionRequest(); diversionRequest.setDiversionKey(diversionKey); if (MapUtils.isNotEmpty(params)) { DiversionCondition condition = new DiversionCondition(); condition.setCustomConditions(params); diversionRequest.setCondition(condition); } ABResult result = xsABTestClient.ab(testKey, diversionRequest); if (result == null || !result.getSuccess()) { return null; } ​ return result.getDiversionResult(); } catch (Exception ex) { log.error("abTest error, testKey:{}, diversionKey:{}", testKey, diversionKey, ex); throw ex; }

我们梳理这段代码中的预算子和运算元以及分别统计出其个数:

为什么开发软件装起来那么复杂,开发一个软件有那么容易吗(2)

根据统计上面统计得到的对应的数据我们进行计算:

为什么开发软件装起来那么复杂,开发一个软件有那么容易吗(3)

Halstead方法优点Halstead 方法的缺点2.2 圈复杂度

圈复杂度(Cyclomatic complexity)是一种代码复杂度的衡量标准,在1976年由Thomas J. McCabe, Sr. 提出。

在软件测试的概念里,圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为线性无关的路径条数,即合理的预防错误所需测试的最少路径条数。圈复杂度大说明程序代码可能质量低且难于测试和维护,根据经验,程序的可能错误和高的圈复杂度有着很大关系,一般来说,圈复杂度大于10的方法存在很大的出错风险。

为什么开发软件装起来那么复杂,开发一个软件有那么容易吗(4)

首页 123下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.