圈复杂度详解以及解决圈复杂度常用的方法

1.什么是代码圈复杂度?

圈复杂度(Cyclomatic Complexity)是一种代码复杂度的衡量标准,由 Thomas McCabe 于 1976年定义。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。圈复杂度大说明程序代码的判断逻辑复杂,可能质量低且难于测试和维护。程序的可能错误和高的圈复杂度有着很大关系。

圈复杂度详解以及解决圈复杂度常用的方法_第1张图片
圈复杂度主要与分支语句(if、else、,switch 等)的个数成正相关。可以在图1中看到常用到的几种语句的控制流图(表示程序执行流程的有向图)。当一段代码中含有较多的分支语句,其逻辑复杂程度就会增加。在计算圈复杂度时,可以通过程序控制流图方便的计算出来。

2.采用圈复杂度去衡量代码的好处

1.指出极复杂模块或方法,这样的模块或方法也许可以进一步细化。
2.限制程序逻辑过长。

McCabe&Associates 公司建议尽可能使 V(G) <= 10。NIST(国家标准技术研究所)认为在一些特定情形下,模块圈复杂度上限放宽到 15 会比较合适。

因此圈复杂度 V(G)与代码质量的关系如下: 
V(G) ∈ [ 0 , 10 ]:代码质量不错; 
V(G) ∈ [ 11 , 15 ]:可能存在需要拆分的代码,应当尽可能想措施重构; 
V(G) ∈ [ 16 , ∞ ):必须进行重构;
3.方便做测试计划,确定测试重点。

许多研究指出一模块及方法的圈复杂度和其中的缺陷个数有相关性,许多这类研究发现圈复杂度和模块或者方法的缺陷个数有正相关的关系:圈复杂度最高的模块及方法,其中的缺陷个数也最多,做测试时做重点测试。

3.代码圈复杂度的计算方法

通常使用的计算公式是V(G) = e – n + 2 , e 代表在控制流图中的边的数量(对应代码中顺序结构的部分),n 代表在控制流图中的节点数量,包括起点和终点(1、所有终点只计算一次,即便有多个return或者throw;2、节点对应代码中的分支语句)。

增加圈复杂度的语句:在代码中的表现形式:在一段代码中含有很多的 if / else 语句或者其他的判定语句(if / else , switch / case , for , while , | | , ? , …)。

圈复杂度详解以及解决圈复杂度常用的方法_第2张图片
代码示例-控制流图

根据公式 V(G) = e – n + 2 = 12 – 8 + 2 = 6 ,上图的圈复杂段为6。

说明一下为什么n = 8,虽然图上的真正节点有12个,但是其中有5个节点为throw、return,这样的节点为end节点,只能记做一个。

4.可以降低圈复杂度的重构技术

1.Extract Method(提炼函数)

void printOwing(double previousAmount)
{
  Enumeration e = _orders.elements();
  double outstanding = previousAmount * 1.2;

  // print banner
  System.out.println ("* * * * * * * * * * * * * * * * * * * * * * *");
  System.out.println ("Customer Owes ");
  System.out.println (" * * * * * * * * * * * * * * * * * * * * * * *");

  // calculate outstanding
  while (e.hasMoreElements())
  {
    Order each = (Order) e.nextElement();
    outstanding += each.getAmount();
  }

  //print details
  System.out.println ("name:" +_name);
  System.out.println ("amount" + outstanding)
}

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

void printOwing(double previousAmount)
{
  printBanner();
  double  outstanding  =  getOutsta nding(previousAmount  *  1.2);
  printDetads(outstanding);
}

void printBanner0
{
   // print banner
.....
}

double getOutstanding(double initialValue)
{
  double result = initialValue;
  Enumeration e =  orders.elements();
  while (e.hasMoreElements())
  {
    Order each = (Order) e.nextElement();
     result += each.getAmount();
  }
  return result;
}

void printDetails (double outstanding)
 {
  //print details
...
}

2.Substitute Algorithm(替换你的算法)

String foundPerson(String[] people)
{
  for (int i = 0; i < people.length; i++)
  {
      if (people[i].equals ("Don"))
       return "Don";
      if (people[i].equals ("John"))
       return "John";
      if (people[i].equals ("Kent"))
       return "Kent";
  }
  return "";
}

将函数本体替换为另一个算法

String foundPerson(String[] people)
{
   List candidates = Arrays.asList(new String[]{"Don", "John","Kent"});
   for (int i=0; i<people.length; i++)
      if (candidates.contains(people[i]))
         return people[i];
  return "";
}

3.Decompose Conditional(分解条件式)

//分解前
if (date.before (SUMMER_START)  || date.after(SUMMER_END))
  charge = quantity * _winterRate  + winterServiceCharge;
else
  charge = quantity * _summerRate;

//分解后 你有一个复杂的条件语句,从if、else段落中分别提炼出独立函数
if (notSummer(date))
  charge = winterCharge(quantity);
else
  charge = summerCharge (quantity);

4.Consolidate Conditional Expression(合并条件式)

例子1

double disabilityAmount()
{
   if (_seniority < 2) return 0;
   if (_monthsDisabled > 12) return 0;
   if (_isPartTime) return 0;
   // compute the disability amount
...
}

//将这些判断合并为一个条件式,并将这个条件式提炼成为一个独立函数,函数名自注释
double disabilityAmount()
{
   if (isNotEligableForDisability()) return 0;
   // compute the disability amount
...
}

5.Consolidate Duplicate Conditional Fragments(合并重复的条件片断)

//在条件式的每个分支上有着相同的一段代码。
if (isSpecialDeal()) 
{
     total = price * 0.95;
     send();
}
else
{
     total = price * 0.98;
     send();
}

//将这段重复代码搬移到条件式之外,避免用拷贝粘贴的方式写重复代码
if (isSpecialDeal())
    total = price * 0.95;
else
    total = price * 0.98;
send();

6.Remove Control Flag(移除控制标记)

//当前代码使用标记变量,可读性差,容易出错
void checkSecurity(String[] people) {
    boolean found = false;
    for (int i = 0; i < people.length; i++) {
        if (! found) {
            if (people[i].equals ("Don")){
                sendAlert();
                found = true;
            }
            if (people[i].equals ("John")){
                   sendAlert();
                   found = true;
            }
        }
    }
}

//以break和return取代标记变量
void checkSecurity(String[] people) {
    for (int i = 0; i < people.length; i++) {     
        if (people[i].equals ("Don")){
            sendAlert();
            break;
        }
        if (people[i].equals ("John")){
            sendAlert();
            break;
        }
    }
}

7.Parameterize Method(令函数携带参数)

//若干函数做了类似的工作,但在函数本体中却包含了不同的值
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);
}

//建立单一函数,以参数表达那些不同的值
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;
    else 
        return 0;
}

8.异常逻辑处理型重构方法

原则:尽可能地维持正常流程代码在最外层。
意思是说,可以写if-else语句时一定要尽量保持主干代码是正常流程,避免嵌套过深。
实现的手段有:减少嵌套、移除临时变量、条件取反判断、合并条件表达式等。

例子1

double getPayAmount(){
    double result;
    if(_isDead) {
        result = deadAmount();
    }else{
        if(_isSeparated){
            result = separatedAmount();
        }
        else{
            if(_isRetired){
                result = retiredAmount();
            else{
                result = normalPayAmount();
            }
        }
    }
    return result;

//重构后   要求梳理清楚逻辑
double getPayAmount(){
    if(_isDead) 
        return deadAmount();
 
    if(_isSeparated)
        return separatedAmount();
 
    if(_isRetired)
        return retiredAmount();
 
    return normalPayAmount();
}

例子2

public double getAdjustedCapital(){
    double result = 0.0;
    if(_capital > 0.0 ){
        if(_intRate > 0 && _duration >0){
            resutl = (_income / _duration) *ADJ_FACTOR;
        }
    }
    return result;
}

//第一步处理 减少嵌套和移除临时变量:
public double getAdjustedCapital(){
    if(_capital <= 0.0 ){
        return 0.0;
    }
    if(_intRate > 0 && _duration >0){
        return (_income / _duration) *ADJ_FACTOR;
    }
    return 0.0;
}

//根据优化原则(尽可能地维持正常流程代码在最外层),可以再继续重构:
public double getAdjustedCapital(){
    if(_capital <= 0.0 ){
        return 0.0;
    }
    if(_intRate <= 0 || _duration <= 0){
        return 0.0;
    }
 
    return (_income / _duration) *ADJ_FACTOR;
}

例子3

   /* 查找年龄大于18岁且为男性的学生列表 */
    public ArrayList<Student> getStudents(int uid){
        ArrayList<Student> result = new ArrayList<Student>();
        Student stu = getStudentByUid(uid);
        if (stu != null) {
            Teacher teacher = stu.getTeacher();
            if(teacher != null){
                ArrayList<Student> students = teacher.getStudents();
                if(students != null){
                    for(Student student : students){
                        if(student.getAge() > = 18 && student.getGender() == MALE){
                            result.add(student);
                        }
                    }
                }else {
                    logger.error("获取学生列表失败");
                }
            }else {
                logger.error("获取老师信息失败");
            }
        } else {
            logger.error("获取学生信息失败");
        }
        return result;
    }

典型的"箭头型"代码,最大的问题是嵌套过深,解决方法是异常条件先退出,保持主干流程是核心流程:

   /* 查找年龄大于18岁且为男性的学生列表 */
    public ArrayList getStudents(int uid){
        ArrayList result = new ArrayList();
        Student stu = getStudentByUid(uid);
        if (stu == null) {
            logger.error("获取学生信息失败");
            return result;
        }
 
        Teacher teacher = stu.getTeacher();
        if(teacher == null){
            logger.error("获取老师信息失败");
            return result;
        }
 
        ArrayList students = teacher.getStudents();
        if(students == null){
            logger.error("获取学生列表失败");
            return result;
        }
 
        for(Student student : students){
            if(student.getAge() > 18 && student.getGender() == MALE){
                result.add(student);
            }
        }
        return result;
    }

9.状态处理型重构方法(1)

double getPayAmount(){
    Object obj = getObj();
    double money = 0;
    if (obj.getType == 1) {
        ObjectA objA = obj.getObjectA();
        money = objA.getMoney()*obj.getNormalMoneryA();
    }
    else if (obj.getType == 2) {
        ObjectB objB = obj.getObjectB();
        money = objB.getMoney()*obj.getNormalMoneryB()+1000;
    }
}

重构后

double getPayAmount(){
    Object obj = getObj();
    if (obj.getType == 1) {
        return getType1Money(obj);
    }
    else if (obj.getType == 2) {
        return getType2Money(obj);
    }
}
 
double getType1Money(Object obj){
    ObjectA objA = obj.getObjectA();
    return objA.getMoney()*obj.getNormalMoneryA();
}
 
double getType2Money(Object obj){
    ObjectB objB = obj.getObjectB();
    return objB.getMoney()*obj.getNormalMoneryB()+1000;
}

10.状态处理型重构方法(1)

针对状态处理的代码,一种优雅的做法是用多态取代条件表达式(《重构》推荐做法)。

double getSpeed(){
    switch(_type){
        case EUROPEAN:
            return getBaseSpeed();
        case AFRICAN:
            return getBaseSpeed()-getLoadFactor()*_numberOfCoconuts;
        case NORWEGIAN_BLUE:
            return (_isNailed)?0:getBaseSpeed(_voltage);
    }
}

重构后

class Bird{
    abstract double getSpeed();
}
 
class European extends Bird{
    double getSpeed(){
        return getBaseSpeed();
    }
}
 
class African extends Bird{
    double getSpeed(){
        return getBaseSpeed()-getLoadFactor()*_numberOfCoconuts;
    }
}
 
class NorwegianBlue extends Bird{
    double getSpeed(){
        return (_isNailed)?0:getBaseSpeed(_voltage);
    }
}

11.case语句重构方法(1)

https://www.cnblogs.com/wahaha02/p/7435603.html

你可能感兴趣的:(圈复杂度)