编写可维护软件的不朽代码随想-3

编写简单的代码单元

每个问题的内部都有许多更小的问题。

原则:

限制每个代码单元分支点的数量不超过4个

评测复杂度的一个常用方式是计算一段代码中可能路径的数量,也就是分支数量,在C#中具体是if和switch语句。

分支覆盖率:一个代码单元分支点的数量,就是覆盖所有分支点产生的分支路径的最小数量。

执行路径:所有分支组合起来就是该代码单元的执行路径,也称为单元路径的最大数量。

分支点的数量是路径的最小值,执行路径是所有路径的最大值,衡量复杂度的标准采用的是分支点数量+1,圈复杂度

没有任何分支点的代码单元只能有一个执行路径,因此也就只会有一个测试用例。

C#中被认为是分支点的语句和操作符:if  case  ?,??  &&,||  while  for,foreach  catch

高复杂度如何产生?

复杂的代码单元都包含许多耦合在一起的代码块,其复杂度就是其中各个代码块的复杂度总和。复杂度会随着if-then-else的嵌套而增加,嵌套越多代码越难以理解。

另外一个原因是使用非常长的链式if-then-else或者switch。对于嵌套的可以采用上一章提到的提取方法来改善。 

 

举个栗子

返回国旗颜色的方法

public IList<Color> GetFlagColors(Nationality nationality)
{
  List<Color> result;
  switch(nationality)
  {
    case Nationality.DUTCH:
      result = new List<Color> { Color.Red, Color.White, Color.Blue };
      break;
    case Nationality.GERMAN:
      result = new List<Color> { Color.Black, Color.Red, Color.Yellow };
      break;
    case Nationality.BELGIAN:
      result = new List<Color> { Color.Black, Color.Yellow, Color.Red };
      break;
    case Nationality.FRENCH:
      result = new List<Color> { Color.Blue, Color.White, Color.Red };
      break;
    case Nationality.ITALIAN:
      result = new List<Color> { Color.Green, Color.White, Color.Red };
      break;
    case Nationality.UNCLASSIFIED:
    default:
      result = new List<Color> { Color.Gray };
    break;
  }
  return result;
}

降低其复杂度的一种方法是 引入一个Map数据结构,将国家映射到指定的Flag对象上。

private static Dictionary<Nationality, IList<Color>> FLAGS = new Dictionary<Nationality, IList<Color>>();
static FlagFactoryWithMap()
{
  FLAGS[Nationality.DUTCH] = new List<Color> { Color.Red, Color.White, Color.Blue };
  FLAGS[Nationality.GERMAN] = new List<Color> { Color.Black, Color.Red, Color.Yellow };
  FLAGS[Nationality.BELGIAN] = new List<Color> { Color.Black, Color.Yellow, Color.Red };
  ...
}

public IList<Color> GetFlagColors(Nationality nationality)
{
  IList<Color> colors = FLAGS[nationality];
  return colors ?? new List<Color> { Color.Gray };
}

 

第二种方法是 将不同国旗的功能拆分到不同的国旗类型中,使用多态来代替条件判断,让每个国旗都拥有一个自己的类型并实现同一接口。

同一接口

public interface IFlag
{
IList<Color> Colors { get; }
}

荷兰国旗
public class DutchFlag : IFlag
{
public IList<Color> Colors { get { new List<Color> { Color.Red, Color.White, Color.Blue }; } }
}

意大利国旗
public class ItalianFlag : IFlag
{
public IList<Color> Colors { get { new List<Color> { Color.Green, Color.White, Color.Red }; } }
}
//其他国家类...


private static readonly Dictionary<Nationality, IFlag> FLAGS = new Dictionary<Nationality, IFlag>();
static FlagFactory()
{
  FLAGS[Nationality.DUTCH] = new DutchFlag();
  FLAGS[Nationality.ITALIAN] = new ItalianFlag();
  //其他国家国旗...
}
public IList<Color> GetFlagColors(Nationality nationality)
{
  IFlag flag = FLAGS[nationality];
  flag = flag ?? new DefaultFlag();
  return flag.Colors;
}

 

处理嵌套条件语句

下面给出了一个二分查找树的根节点和一个整数,计算出整数在树中的位置。如果找到就返回整数在树中的深度,否则抛出一个异常。

public static int CalculateDepth(BinaryTreeNode<int> t, int n)
{
  int depth = 0;
  if (t.value == n)
  {
    return depth;
  }
  else
  {
    if (n < t.value)
    {
      BinaryTreeNode<int> left = t.left;
      if (left == null)
      {
        throw new TreeException("value not found in tree");
      }
      else
      {
        return 1 + CalculateDepth(left, n);
      }
    }
    else
    {
      BinaryTreeNode<int> right = t.right;
      if (right == null)
      {
        throw new TreeException("value not found in tree");
      }
      else
      {
        return 1 + CalculateDepth(right, n);
      }
    }
  }
}

为提高可读性,可以标识出各种独立的情况,并插入return语句来代替嵌套的条件语句,这种做法称为 使用卫语句来代替嵌套的条件语句

如果条件语句极其复杂,就应该将条件语句拆解开,然后逐个检查,并在条件为真时立刻从函数中返回,这样的单独检查通常被称之为“卫语句”(guard clauses)

卫语句的效果就是将原来需要仔细阅读代码、细心整理逻辑的条件判断整理成一眼能看透的逻辑关系,效果就像以下:

if(it == 活的){
    if(it == 人){

        if(it != 女人){
            return 不喜欢;

        } else {
            return 喜欢;

        }

    } else {
        return 不喜欢;

    }

} else {
    return 不喜欢;

}

以上代码无非就是表达 我只喜欢活的女人 的意思,但是代码一层套一层明显妨碍了我在第一时间获取最核心信息的效率,使用卫语句来表达就是:

if (it != 活的){return 不喜欢}

if(it!=人){return 不喜欢}

if(it!=女人){return 不喜欢}

if(it == 女人 && it == 活的) {return 喜欢}

if(其他任何情况){return 不喜欢}

大概就是这么个东西,当然这些代码有重复的逻辑判断在里面。

public static int CalculateDepth(BinaryTreeNode<int> t, int n)
{
  int depth = 0;
  if (t.value == n)
  {
    return depth;
  }
  if ((n < t.value) && (t.left != null))
  {
    return 1 + CalculateDepth(left, n);
  }
  if ((n > t.value) && (t.right != null))
  {
    return 1 + CalculateDepth(right, n);
  }
  throw new TreeException("value not found in tree");
}

为了降低复杂度,还需要将嵌套的条件语句提取到其他的方法中:

public static int CalculateDepth(BinaryTreeNode<int> t, int n)
{
  int depth = 0;
  if (t.value == n)
  {
    return depth;
  }
  else
  {
    return TraverseByValue(t, n);
  }
}

private static int TraverseByValue(BinaryTreeNode<int> t, int n)
{
  BinaryTreeNode<int> childNode = GetChildNode(t, n);
  if(childNode==null)
  {
    throw new TreeException("value not found in tree");
  }
  else
  {
    return 1 + CalculateDepth(childNode, n);
  }
}
private static BinaryTreeNode<int> GetChildNode(BinaryTreeNode<int> t, int n)
{
  if (n < t.value)
  {
    return t.left;
  }
  else
  {
    return t.right;
  }
}

 

反对意见:

高复杂度不可避免。复杂的领域并不一定要求技术实现也复杂,开发人员的责任就是要简化问题,编写简单的代码。不过,领域越复杂,开发人员就越花费更多的精力构建简单的技术方案,这个完全可以做到!

拆分方法并不会降低复杂度,比如将复杂度15的方法替换为3个复杂度为5的方法,总的还是15,感觉没啥好处。要从可维护性来看,他会变得跟容易测试,也更容易理解

 

SIG评价代码单元复杂度,分4星评级,超过25,超过10,超过5,低于5

 

posted @ 2021-03-30 19:18  小么me  阅读(90)  评论(0)    收藏  举报