10分钟了解圈复杂度

背景

常听同事们讲某一段代码复杂度高,不好理解。那么什么样的代码算是复杂度高呢?

python 复制代码
if A:
    for B in range(10):
        if B % 2 == 1:
            print B
        else:
            for C in range(10):
                if C % 2 == 1:
                    print C
else if D:
        print D
    else:
        if E:
            pass
        else:
            for F in range(10):
                print F

以上代码的直观上看复杂度就挺高。解决问题的前提是先量化问题,圈复杂度就是对代码复杂度的量化。这段代码的圈复杂度为8。一般来说,圈复杂度大于7的函数就算是复杂函数了。

概念

圈复杂度(Cyclomatic complexity),是一种软件度量。用来表示程序的复杂度。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。

计算方法

方法一

V(G) = E - N + 2

其中,e表示控制流图中边的数量,n表示控制流图中节点的数量。下面是典型的控制流程,如if-else,While,until和正常的流程顺序:

方法二

V (G) = P + 1

P表示控制节点的数量。 其中P为判定节点数,判定节点举例:

  1. if语句
  2. while语句
  3. for语句
  4. case语句
  5. catch语句
  6. and和or布尔操作
  7. ?:三元运算符

举例

css 复制代码
A = 10
   IF B > C THEN
      A = B
   ELSE
      A = C
   ENDIF
Print A
Print B
Print C

以上代码的控制流为

  • 根据方法一:V(G) = 7-7+2 = 2
  • 根据方法二:V(G) = 1+1 = 2

优化方法

重新组织你的函数

技巧1 提炼函数

有一段代码可以被组织在一起并独立出来:

scss 复制代码
void Example(int val)
{
	if( val > MAX_VAL)
	{
		val = MAX_VAL;
	}

	for( int i = 0; i < val; i++)
	{
		doSomething(i);
	}
}

将这段代码放进一个独立函数中,并让函数名称解释该函数的用途:

scss 复制代码
int getValidVal(int val)
{
   	if( val > MAX_VAL)
	{
		return MAX_VAL;
	} 
    return val;
}

void doSomethings(int val)
{
	for( int i = 0; i < val; i++)
	{
		doSomething(i);
	}
}

void Example(int val)
{
    doSomethings(getValidVal(val));
}

最后还要重新审视函数内容是否在统一层次上。

技巧2 替换算法

把某个算法替换为另一个更清晰的算法:

c 复制代码
string foundPerson(const vector<string>& peoples){
  for (auto& people : peoples) 
  {
    if (people == "Don"){
      return "Don";
    }
    if (people == "John"){
      return "John";
    }
    if (people == "Kent"){
      return "Kent";
    }
  }
  return "";
}

将函数实现替换为另一个算法:

c 复制代码
string foundPerson(const vector<string>& people){
  std::map<string,string>candidates{
    	{ "Don", "Don"},
    	{ "John", "John"},
    	{ "Kent", "Kent"},
       };
  for (auto& people : peoples) 
  {
    auto& it = candidates.find(people);
    if(it != candidates.end())
        return it->second;
  }
}

所谓的表驱动。

简化条件表达式

技巧3 逆向表达

在代码中可能存在条件表达如下:

kotlin 复制代码
if ((condition1() && condition2()) || !condition1())
{
    return true;
}
else
{
    return false;
}

应用逆向表达调换表达顺序后效果如下:

kotlin 复制代码
if(condition1() && !condition2())
{
    return false;
}

return true;
技巧4 分解条件

在代码中存在复杂的条件表达:

ini 复制代码
if(date.before (SUMMER_START) || date.after(SUMMER_END))
    charge = quantity * _winterRate + _winterServiceCharge;
else 
    charge = quantity * _summerRate;

从if、then、else三个段落中分别提炼出独立函数:

ini 复制代码
if(notSummer(date))
    charge = winterCharge(quantity);
else 
    charge = summerCharge (quantity);
技巧5 合并条件

一系列条件判断,都得到相同结果:

csharp 复制代码
double disabilityAmount() 
{
    if (_seniority < 2) return 0;
    if (_monthsDisabled > 12) return 0;
    if (_isPartTime) return 0;
    // compute the disability amount
    ......

将这些判断合并为一个条件式,并将这个条件式提炼成为一个独立函数:

csharp 复制代码
double disabilityAmount() 
{
    if (isNotEligableForDisability()) return 0;
    // compute the disability amount
    ......
技巧6 移除控制标记

在代码逻辑中,有时候会使用bool类型作为逻辑控制标记:

ini 复制代码
void checkSecurity(vector<string>& peoples) {
	bool found = false;
	for (auto& people : peoples) 
    {
		if (! found) {
			if (people == "Don"){
				sendAlert();
				found = true;
			}
			if (people == "John"){
				   sendAlert();
				   found = true;
			}
		}
	}
}

使用break和return取代控制标记:

c 复制代码
void checkSecurity(vector<string>& peoples) {
	for (auto& people : peoples)
	{     
		if (people == "Don" || people == "John")
		{
			sendAlert();
			break;
		}
	}
}
技巧7 以多态取代条件式

条件式根据对象类型的不同而选择不同的行为:

csharp 复制代码
double getSpeed() 
{
    switch (_type) {
        case EUROPEAN:
            return getBaseSpeed();
        case AFRICAN:
            return getBaseSpeed() - getLoadFactor() *_numberOfCoconuts;
        case NORWEGIAN_BLUE:
            return (_isNailed) ? 0 : getBaseSpeed(_voltage);
    }
    throw new RuntimeException ("Should be unreachable");
}

将整个条件式的每个分支放进一个子类的重载方法中,然后将原始函数声明为抽象方法:

csharp 复制代码
class Bird
{
public:
    virtual double getSpeed() = 0;
    
protected:
    double getBaseSpeed();
}

class EuropeanBird
{
public:
    double getSpeed()
    {
        return getBaseSpeed();
    }
}

class AfricanBird
{
public:
    double getSpeed()
    {
        return getBaseSpeed() - getLoadFactor() *_numberOfCoconuts;
    }
    
private:
    double getLoadFactor();
    
    double _numberOfCoconuts;
}

class NorwegianBlueBird
{
public:
    double getSpeed()
    {
        return (_isNailed) ? 0 : getBaseSpeed(_voltage);
    };
    
private:
    bool _isNailed;
}

简化函数调用

技巧8 读写分离

某个函数既返回对象状态值,又修改对象状态:

arduino 复制代码
class Customer
{
int getTotalOutstandingAndSetReadyForSummaries(int number);
}

建立两个不同的函数,其中一个负责查询,另一个负责修改:

csharp 复制代码
class Customer
{
    int getTotalOutstanding();
    void SetReadyForSummaries(int number);
}
技巧9 参数化方法

若干函数做了类似的工作,但在函数本体中却 包含了不同的值:

scss 复制代码
Dollars baseCharge()
 {
    double result = Math.min(lastUsage(),100) * 0.03;
    if (lastUsage() > 100)
    {
        result += (Math.min (lastUsage(),200) - 100) * 0.05;
    }
    if (lastUsage() > 200)
    {
        result += (lastUsage() - 200) * 0.07;
    }
    return new Dollars (result);
}

建立单一函数,以参数表达那些不同的值:

sql 复制代码
Dollars baseCharge() 
{
    double result = usageInRange(0, 100) * 0.03;
    result += usageInRange (100,200) * 0.05;
    result += usageInRange (200, Integer.MAX_VALUE) * 0.07;
    return new Dollars (result);
}

int usageInRange(int start, int end) 
{
    if (lastUsage() > start) 
        return Math.min(lastUsage(),end) -start;
     
    return 0;
}
技巧10 以明确函数取代参数

函数实现完全取决于参数值而采取不同反应:

ini 复制代码
void setValue (string name, int value) 
{
    if (name == "height")
        _height = value;
    else if (name == "width")
        _width = value;
    Assert.shouldNeverReachHere();
}

针对该参数的每一个可能值,建立一个独立函数:

arduino 复制代码
void setHeight(int arg) 
{
    _height = arg;
}
void setWidth (int arg) 
{
    _width = arg;
}
相关推荐
喵手11 分钟前
如何利用Java的Stream API提高代码的简洁度和效率?
java·后端·java ee
掘金码甲哥18 分钟前
全网最全的跨域资源共享CORS方案分析
后端
m0_4805026425 分钟前
Rust 入门 生命周期-next2 (十九)
开发语言·后端·rust
张醒言31 分钟前
Protocol Buffers 中 optional 关键字的发展史
后端·rpc·protobuf
鹿鹿的布丁1 小时前
通过Lua脚本多个网关循环外呼
后端
墨子白1 小时前
application.yml 文件必须配置哇
后端
xcya1 小时前
Java ReentrantLock 核心用法
后端
用户466537015051 小时前
如何在 IntelliJ IDEA 中可视化压缩提交到生产分支
后端·github
小楓12011 小时前
MySQL數據庫開發教學(一) 基本架構
数据库·后端·mysql
天天摸鱼的java工程师1 小时前
Java 解析 JSON 文件:八年老开发的实战总结(从业务到代码)
java·后端·面试