保守容易性指数(Maintainability Idex)の計算方法を理解する


概要

最近、flutterでコードメトリクス1を計測したら、保守容易性指数(Maintainability Idex)という指標が出てきました。

ちょっと調べて見たところ、C#とかでは結構メジャーっぽい(?)。
どうやって改善するのかわからないので調査します。
(最近まで全然知らんかったんだけど、この指標みんな使ってるの?)

目次

保守容易性指数の目安

コードメトリックス-保守容易性インデックスの範囲と意味(Microsoft)によると、評価基準は以下の通りらしい。

  • 0-9 = 赤
  • 10-19 = 黄
  • 20-100 = 緑

ただし、コレはMicrosoftの2保守容易性指数(Maintainability Idex)なので、他のツールだとちょっと違うかも。
でもこの基準を参照すると、僕のflutterコードはめっちゃきれいですやん。

保守容易性指数の計算式

いろいろなところで計算式は紹介されていますが、概ね2以下のとおりです

保守容易性指数
= 171 - 5.2 * log(Halstead複雑度のVolume) - 0.23 * サイクロマチック複雑度 - 16.2 * log(コードのライン数)

まず、 マジックナンバー(171とか)については諦めます
流石に調査はキツイので、誰が知っている人がいたら教えてください...

したがって、このページでは'サイクロマチック複雑度'と'Halstead複雑度のVolume'の具体的な計算方法を紹介します。

サイクロマチック複雑度

概要

英語版のWikipediaを引用します。

ソースコードのサイクロマチック複雑度は対象のセクションにおける'線形独立'なパスの数です。
'線形独立'とは、他のパスに存在しないエッジが1つ以上存在することです。

具体例

以下の関数のサイクロマチック複雑度を計算します。

int test_function(int max_index){
  if (max_index == 0) return 0;

  for (int index = 0; index < max_index; ++index){
    // 処理A
    if (index == 2){
      // 処理B
    }
  }

  return 1;
}

この関数のCFD3は以下のようになります。(レイアウトのため横書きにしています)

では、Entry PointからExitまでの'線形独立'なパスのパターン数を数えます。

上図の通り、4通りのパスが存在するため、サイクロマチック複雑度は4です。
また、Wikipediaには以下のように人間にとって簡単な計算方法が紹介されています

サイクロマチック複雑度Mは以下のように計算されます
M = E - N + 2P
E = CFD3のグラフのエッジ数
N = CFD3のグラフのノード数
P = CFD3の連結グラフ4の数

連結グラフ4の定義を参照すると、1つの関数のCFDではP=1だと言うことがわかります。
したがって、

M = E - N + 2

平面においては面の数=エッジ数-ノード数+2であるため、1つの関数のサイクロマチック複雑度MはCFD3の面の数を数えれば良いことがわかります。

Halstead複雑度のVolume

英語版のWikipediaのHalstead複雑度の項では以下の3種類の指標が定義されています

  • Volume
  • Difficulty
  • Effort

ここでは、保守容易性指数(Maintainability Idex)の計算に必要なVolumeの概要と具体例を記述します。

概要

プログラムの長さ・大きさを表現する指標です。ざっくりいうと以下のように定義されています。

Halstead複雑度のVolume = 単語の数 * 1単語あたりのビット数

つまり、Halstead複雑度のVolumeとは プログラム全体をビット配列で表現したときの長さ のことです。

ココで言う'単語'はオペレータ5とオペランド5のことです。
しかしながら、単純にトークンのことだと考えても結果に大して違いはない(はず)です。

具体例

英語版Wikipediaの例をそのまま引用します。

main()
{
    int a, b, c, avg;
    scanf("%d %d %d", &a, &b, &c);
    avg = (a + b + c) / 3;
    printf("avg = %d", avg);
}

まず、1単語あたりのビット数を計算します。このプログラムには、以下の12種類の'単語'が出現します。

  1. main
  2. ()
  3. {}
  4. int
  5. scanf
  6. &
  7. =
  8. +
  9. /
  10. printf
  11. ,
  12. ;
  13. a
  14. b
  15. c
  16. avg
  17. "%d %d %d"
  18. 3
  19. "avg = %d"

これをビット配列で表現するとき、必要なビット数はlog2(19)です。コレが'1単語あたりのビット数'になります。
次に、'単語の数'を数えると、27個存在することがわかります。

したがって、Halstead複雑度のVolume = 27 *log2(19)となります。

最後に

Halstead複雑度のVolumeは複雑な解説資料多いけど、ちゃんと資料読んだら概念はめっちゃ単純でした。
ここまで読むと、以下の式で算出される値を改善する方法もなんとなくわかってくると思います。

保守容易性指数
= 171 - 5.2 * log(Halstead複雑度のVolume) - 0.23 * サイクロマチック複雑度 - 16.2 * log(コードのライン数)

参考にした資料


  1. https://pub.dev/packages/dart_code_metrics 

  2. 場所によって0-100の値に正規化していたり、コメント行数を考慮した式が追加されていたりします 

  3. Control Flow Diagram。clangを使えば自動的に生成もできる 

  4. 連結グラフ 

  5. ググってくれ!