代码圈复杂度治理小结
作者 | 陈胜利(李渔)
来源 | 阿里开发者公众号
网上有个段子,说建筑工程师不会轻易答应会给摩天大楼增加一个地下室,但代码开发工程师却经常在干这样的事,并且总有人会对你说“这个需求很简单”。到土里埋个雷,这确实不复杂,但我们往往面临的真实场景其实是“在一片雷区的土里埋一个雷”。而雷区里哪里有雷,任何人都不知道。
回到我们日常的写代码的场景,我们一直在说系统很复杂,那到底什么是系统复杂度呢?最近几个月,蚂蚁代码力平台(注:是蚂蚁的代码评价平台)进入大家视野,很多同学开始关注起自己代码力的得分情况。作为团队的稳定性底盘负责人,也经常和大家探讨为什么会因为圈复杂度高而被扣分。那么,怎么才能写的一手可读,可扩展,可维护[注1]的好代码?本文作者尝试结合在团队内部的实践,分享下过程中心得,希望对大家的代码圈复杂度治理提供微弱的帮助。
什么是圈复杂度
先看看圈复杂度的通用的定义,圈复杂度(Cyclomatic complexity,简写CC)[注2]也称为条件复杂度/循环复杂度,是一种代码复杂度的衡量标准。由托马斯·J·麦凯布(Thomas J. McCabe, Sr.)于1976年提出,用来表示程序的复杂度,其符号为VG。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。说人话,圈复杂度关系到质量同学最少需要设计多少用例才能覆盖你的代码分支。
怎么计算圈复杂度
蚂蚁广目平台给出了比较详细的说明,这里直接引用,网上也可以查到类似内容。
节点判断计算公式为:V (G) = P + 1 注:除了节点判断法,还有其他方法,如点边判断法,这里只选一个用于说明。
其中P为条件节点数,条件节点类型为:
a.条件语句
- if语句
- while语句(包含do...while...语句)
- for语句(包含foreach语句)
- switch case语句
- try catch语句
b.条件表达式(二元或多元)
- && 表达式
- || 表达式
- 三元运算符
举例如下(部分代码省略后用xxx代替):
//案例1,圈复杂度V(G) = 1(if) + 1(catch) + 1 = 3
public String myMethod1(){
if(xxx){
try {
//xxx;
} catch (IOException e) {
//xxx;
}
}else{
xxx;
}
return xx;
}
//案例2,圈复杂度V(G) = 2(if) + 1(&&) + 1 = 4
public String myMethod2() {
if (xxx) {
//xxx;
} else {
if (xxx && xxx) {
//xxx;
} else {
//xxx;
}
xx();
}
return xx;
}
为什么要关注圈复杂度
好了,了解了圈复杂度的定义之后,我们基本可以得出一个结论,圈复杂度大说明程序逻辑复杂,不利于代码的阅读,维护,和后续扩展。如果需要看懂一个圈复杂度高的方法,需要小心翼翼整理所有的分支情况,而改动这类代码更像踏入雷区一样。
下面,我们来看一段代码案例(部分内容已省略)
public XXresult doSave( XXDTO newScriptDTO) {
String type = Enums.ScriptType.CUSTOM;
Boolean containsTryCatch = StringUtil.contains(content, "try")
&& StringUtil.contains(content, "catch");
if (StringUtil.isBlank(scriptName)) {
baseOperationResult.setMessage("XXX");
return baseOperationResult;
}
if (!scriptName.matches("^[(\\d)|_|a-z|A-Z]+$")) {
baseOperationResult.setMessage("XXX");
return baseOperationResult;
}
NewScript tempScript = null;
try {
tempScript = newScriptManager.findByName(StringUtil.trim(scriptName));
} catch (Exception e) {
baseOperationResult.setMessage("XXX");
return baseOperationResult;
}
if (StringUtil.isBlank(id)) {
if (tempScript != null) {
baseOperationResult.setMessage("XXX");
return baseOperationResult;
}
} else {
Integer editScriptId = Integer.parseInt(id);
if (null != tempScript) {
if (!editScriptId.equals(tempScript.getId())) {
baseOperationResult.setMessage("XXX");
return baseOperationResult;
}
}
}
if (!Enums.NewScriptTypeEnum.XX.contains(scriptType)) {
baseOperationResult.setMessage("XX");
return baseOperationResult;
}
Boolean needSubtypeMode = true;
if (StringUtils.equals(scriptType, Enums.XX.XX)
|| StringUtils.equals(scriptType, Enums.XX.PRE)) {
needSubtypeMode = false;
}
NewScript script = new NewScript();
script.setScriptType(scriptType);
if (StringUtil.isNumeric(status)) {
script.setStatus(Integer.parseInt(status));
}
if (StringUtil.isNotBlank(scriptCategory)) {
script.setScriptCategory(ScriptCategory.getByCode(scriptCategory));
}
String subType = "";
if (needSubtypeMode) {
if (StringUtil.isBlank(subtypeandtip)) {
baseOperationResult.setMessage("XXX");
return baseOperationResult;
}
}
if (needSubtypeMode) {
List< NewScript> allActiveAndTestRunScripts = newScriptManager
.findAllActiveAndTestRunScripts();
List< String> allActiveAndTestRunSubTypeList = new ArrayList<>();
for (NewScript activeAndTestRunScript : allActiveAndTestRunScripts) {
List< String> subTypeListEveryScript = Arrays
.asList(Optional.ofNullable(activeAndTestRunScript.getSubType())
.orElse(new String()).split(","));
for (String subTypeTemp : subTypeListEveryScript) {
if (StringUtil.isNotBlank(subTypeTemp)) {
allActiveAndTestRunSubTypeList.add(subTypeTemp);
}
}
}
try {
JSONArray subtypetipsArray = JSON.parseArray(subtypeandtip);
if (StringUtil.isBlank(id)) {
for (Object object : subtypetipsArray) {
JSONObject subtypetipsObject = (JSONObject) object;
String subtypeSingle = subtypetipsObject.getString("subtype");
if (StringUtil.isBlank(subtypeSingle)) {
baseOperationResult.setSuccess(false);
return baseOperationResult;
}
if (CollectionUtils.contains(allActiveAndTestRunSubTypeList.iterator(),
subtypeSingle)) {
baseOperationResult.setSuccess(false);
return baseOperationResult;
}
}
} else {
if ("1".equals(status) || "2".equals(status)) {
for (Object object : subtypetipsArray) {
//省略部分内容XXX;
if (StringUtil.isBlank(subtypeSingle)) {
baseOperationResult.setSuccess(false);
return baseOperationResult;
}
for (NewScript oldNewScript : allActiveAndTestRunScripts) {
if (oldNewScript.getId().equals(Integer.parseInt(id))) {
continue;
}
//省略部分内容XXX;
if (CollectionUtils.contains(filtered.iterator(), subtypeSingle)) {
baseOperationResult.setSuccess(false);
return baseOperationResult;
}
}
}
}
}
for (Object object : subtypetipsArray) {
if (1 == script.getStatus() || 2 == script.getStatus()) {
SubtypeTips subtypeTips = null;
subtypeTips = subtypeTipsManager.findBySubtype(subtypeSingle);
if (subtypeTips == null) {
subtypeTips = new SubtypeTips();
}
subtypeTips.setSubtype(subtypeSingle);
subtypeTips.setInternalTips(innertips);
subtypeTips.setExternalTips(externaltips);
subtypeTips.setShareLink(shareLink);
subtypeTips.setStatus(1);
subtypeTipsManager.save(subtypeTips);
}
}
subType = StringUtil.substring(subType, 0, subType.length() - 1);
} catch (Exception e) {
baseOperationResult.setSuccess(false);
baseOperationResult.setMessage("XXX");
return baseOperationResult;
}
}
boolean needCreateTestRunScript = false;
if (StringUtils.isNotBlank(id)) {
script.setId(Integer.parseInt(id));
NewScript orgin = newScriptManager.findById(Integer.parseInt(id));
if (null != orgin && 1 == orgin.getStatus() && "1".equals(status)) {
if (StringUtil.isNotBlank(orgin.getContent())) {
String originContentHash = CodeUtil
.getMd5(StringUtil.deleteWhitespace(orgin.getContent()));
String contentHash = CodeUtil.getMd5(StringUtil.deleteWhitespace(content));
if (!StringUtil.equals(originContentHash, contentHash)) {
needCreateTestRunScript = true;
}
}
}
} else {
script.setSubmitter(user.getLoginName());
}
Set< String> systemList = new HashSet< String>();
if (StringUtil.isNotBlank(systems)) {
String[] systemArray = systems.split(",");
for (int i = 0; i < systemArray.length; i++) {
systemList.add(systemArray[i]);
}
}
if (needCreateTestRunScript) {
if (needSubtypeMode) {
content = replaceContent(content, subType);
String testScriptSubType = "";
List< String> subTypeList = Arrays.asList(StringUtil.split(subType, ","));
for (int i = 0; i < subTypeList.size(); i++) {
testScriptSubType += this.UPDATE_SCRIPT + subTypeList.get(i);
if (i != subTypeList.size() - 1) {
testScriptSubType += ",";
}
}
subType = testScriptSubType;
}
scriptName = this.UPDATE_SCRIPT + scriptName;
NewScript oldUpdateScript = newScriptManager.findByName(scriptName);
if (null != oldUpdateScript)
script.setId(oldUpdateScript.getId());
else {
script.setId(null);
}
baseOperationResult.setNeedAudit(true);
}
if (StringUtil.isBlank(fileSuffix)) {
//如果全空的话 默认全扫
script.setSuffix(".*");
} else {
script.setSuffix(fileSuffix);
}
script.setName(scriptName);
if (StringUtil.equals(allPath, "Y")) {
script.setAllPath("Y");
} else {
script.setAllPath("");
}
script.setEnvTag(tenantScope);
script.setNeedAutoScan(needAutoScan);
if (StringUtil.isNotBlank(scopes)) {
for (String each : StringUtil.split(scopes, ",")) {
each = StringUtil.replace(each, " ", "");
script.addScope(each);
}
}
if (StringUtil.isNotBlank(content)) {
BaseOperationResult preLoadResult = syntaxCheck(script);
if (!preLoadResult.isSuccess()) {
baseOperationResult.setMessage(preLoadResult.getMessage());
return baseOperationResult;
}
}
if (StringUtil.contains(content, "new Bug")) {
baseOperationResult.setSuccess(false);
return baseOperationResult;
}
try {
Result< NewScript> result = newScriptManager.saveCustomScript(script);
if (result.isSuccess()) {
if (EnvUtil.isProdEnv() && EnvUtil.isLinux()) {
if (!needCreateTestRunScript) {
//省略部分内容XX
} else {
//省略部分内容XX
}
}
Boolean hasOldScript = processOldEngineRule(scriptName)