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;
}
相关推荐
刚正的热带野猪8 分钟前
文件格式校验方案
java·后端
Anthony_492612 分钟前
深入理解MySQL事务:从版本链到MVCC的全面解析
数据库·后端·mysql
Postkarte不想说话14 分钟前
JSON序列化与反序列化-----使用JSON for Modern C++库
后端
五行星辰16 分钟前
SpringBoot集成Log4j2终极指南:从基础配置到性能调优
java·后端
Fw9965332 分钟前
Spring如何解决获取到不完整Bean的问题
后端
Aska_Lv1 小时前
springboot-tomcat 线程处理web接口解读
后端
苏三说技术1 小时前
千万级大表的优化技巧
后端
天上掉下来个程小白1 小时前
Redis-06.Redis常用命令-列表操作命令
java·redis·后端·springboot·苍穹外卖
掘金码甲哥1 小时前
双份请求,双倍快乐
后端
宦如云1 小时前
PHP语言的分布式账本
开发语言·后端·golang