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;
}
相关推荐
阿杆几秒前
😡同事查日志太慢,我现场教他一套 grep 组合拳!
linux·后端
PetterHillWater1 分钟前
基于Trae智能复杂项目重构实践
后端·aigc
凌览15 分钟前
有了 25k Star 的MediaCrawler爬虫库加持,三分钟搞定某红书、某音等平台爬取!
前端·后端·python
这里有鱼汤26 分钟前
给你的DeepSeek装上实时行情,让他帮你炒股
后端·python·mcp
咖啡啡不加糖28 分钟前
暴力破解漏洞与命令执行漏洞
java·后端·web安全
风象南31 分钟前
SpringBoot敏感配置项加密与解密实战
java·spring boot·后端
ん贤1 小时前
RESTful风格
后端·go·restful
Humbunklung1 小时前
Rust方法语法:赋予结构体行为的力量
开发语言·后端·rust
萧曵 丶1 小时前
Rust 内存结构:深入解析
开发语言·后端·rust
Kookoos1 小时前
ABP VNext + Cosmos DB Change Feed:搭建实时数据变更流服务
数据库·分布式·后端·abp vnext·azure cosmos