【PHP】CyclomaticComplexityとは...?
たかぽん
どーも!

たかぽんです!

今回はPHPを使っていてごく稀に聞くCyclomaticComplexityについて簡単にみていこうと思います!

CyclomaticComplexityとは?

さて、それでは早速ですが、CyclomaticComplexityとはなんでしょうか・・・?

日本語に訳すと"循環的複雑度"です。

・・・。

・・・。

・・・。

は?

わかります。その気持ち。

もっとわかりやすくいうと、プログラムがどれくらい複雑なのかを表す値です。

この値をみることでそのプログラムがどれくらい複雑なのか?扱いづらいのか?がわかるようになっています。

あとでもっと詳しく説明しますね。

で!

本来、この用語は一般的なプログラム全般に言えること何ですが...

今回はあくまで、"PHPMD(PHP Mess Detector)"でのCyclomaticComplexityについて解説していこうと思います!

PHP MDとは・・・?

PHP MDとは、PHP Mess Detectorの略です。

英語に堪能な方はもうお分かりかもしれませんが、Mess(ごちゃごちゃしたもの)をDetector(発見)してくれるわけです。

わかりやすく言い換えると、"PHPのコードからバグの温床になりがちな箇所や使われてない変数、複雑な箇所などなどを見つけて教えてくれるツール"です。

It takes a given PHP source code base and look for several potential problems within that source. These problems can be things like:
・Possible bugs
・Suboptimal code
・Overcomplicated expressions
・Unused parameters, methods, properties

PHP Mess Detector - about

他の言語や解析ツールでもある程度似た内容になっているとは思いますが、あくまで参考程度に。

何故PHPMDなのか・・・ですが、僕が業務で使ってるからです...w

では、詳しく見ていきましょう!

PHP MDにおけるCyclomaticComplexity

さて、それではPHP MDにおけるCyclomaticComplexityとは何なのか?

見ていこうと思います!

全文英語ではありますが、以下公式リファレンスに沿って解説をしますので、よければ見比べながらいってみてください。

まず、”複雑さ”とは、decision points(分岐点の数)で表します。

ここでいう"複雑さ"はCyclomaticComplexity(循環的複雑度)と捉えていただいて大丈夫だと思います。

つまり、CyclomaticComplexityはdecision pointsの数で表されます。

さて、ここで疑問が、decision points(分岐点)とは一体・・・何でしょうか・・・?

直後に解説がなされています。

分岐点は'if', 'while', 'for', と 'case labels'で、一般的に、1~4個以内であれば低、5~7は中、8~10は高、それ以上は複雑さがとても高いと分類されているようですね。

"case labels"は、switch文の"case"の数です。(defaultは含まれません)

なので、php MDからいわせてみれば、分岐点、つまりif文が10箇所くらいあったらもうそのプログラムは複雑さ超たけーよ!おまえ!気をつけろよ!って怒られるわけです。

実際、僕も怒られたからこうして調べました...orz

で、気をつけて欲しいのが、次の一文。

Complexity is determined by the number of decision points in a method plus one for the method entry.

複雑さはメソッド中のdecision pointsとメソッドのエントリー(入り口)分の1を合計した値だよ〜ってことらしいです。

つまり、メソッドがある時点で1はあるよ〜ってことですね。

これは、メソッドを通らない経路一通りの複雑さが、メソッドを通る経路とメソッドを通らない経路の二通りに分岐して+1になるためでしょう。

さて、以上を踏まえて公式ページにある例をみてみます。

// Cyclomatic Complexity = 11
class Foo {
1   public function example() { // <- エントリー分の1
2       if ($a == $b) {
3           if ($a1 == $b1) {
                fiddle();
4           } elseif ($a2 == $b2) {
                fiddle();
            } else {
                fiddle();
            }
5       } elseif ($c == $d) {
6           while ($c == $d) {
                fiddle();
            }
7        } elseif ($e == $f) {
8           for ($n = 0; $n < $h; $n++) {
                fiddle();
            }
        } else {
            switch ($z) {
9               case 1:
                    fiddle();
                    break;
10              case 2:
                    fiddle();
                    break;
11              case 3:
                    fiddle();
                    break;
                default:
                    fiddle();
                    break;
            }
        }
    }
}

上記例のFooクラスでは、CyclomaticComplexity は11になっています。

気をつけて欲しいのが、

  • メソッドのエントリー分で1が加算されていること
  • elseのみの場合はカウントされていないこと
  • defaultはカウントされていないこと

です。

メソッドのエントリー分

メソッドのエントリー分に関しては先に説明した通りです。

elseのみの場合はカウントされない

elseのみの場合はカウントされていませんね。

なんとなぁくですが、elseにも条件はある(if文の条件の否定)ので、そこで加算すべきでは?と思うかもしれません。(筆者はそうでした。

ただ、確証はないのですが、elseのみの場合カウントされていない理由はおそらく...

その瞬間瞬間でのあり得る経路の可能性を考えるとわかるかと思います。

//この瞬間の経路の数は1つ

// ここから2つの経路に分かれる 1 -> 2
if(a > b){
  // 真の経路
} else {
  // 偽の経路
}

if分の条件が読まれた瞬間、今まで1つだった経路が2つ(真偽)に分かれます。

ちょうど一本道の途中に看板があって、そこに男性は右、女性は左と書かれているようなのを想像していただければいいかなと。

すると、if文の条件が読まれた瞬間に今まで1だった経路が2つになるわけです。

ifとelseで二つあるから複雑さが+2にはならないの?と思いますが...

もともと一つの経路があり、それが二つの経路になります。

そのため、+1なんです。

もちろん、ifだけの場合でも経路は二つに分岐します。

ifだけなら一見経路は1つじゃないの?と思われがちですが...

elseを書いてみればはっきりわかります。

///////////// ifの例1

// この瞬間は経路の数は1つ
if(a>b){
  // 真の経路
}

/////////////  ifの例2

// この瞬間は経路の数は1つ
if(a>b){
  // 真の経路
} else {
  // 偽の経路
  // 何もしない
}

上記の二つのif文は全く同じ動作をします。

後者のように何もしないelseを書いてあげるとわかりやすいでしょう。

ifだけの場合でも、ifの中を実行する場合と、elseで何もせず後続処理を行う場合で二通りに分岐するはずです。

上記のコードの経路(ifの中とelseの中)を全て通ろうとすると、二通りになりますね。

もしもこのifがなければ一つの経路でいいんですが、ifがある場合は二通りになります。

もともと一つの経路だけど、二つになったので"複雑さが+1"なわけです。

そういったことから、elseがあってもif分の1カウントしかされないわけです。

公式のリファレンスでは"if"があったら〜と書いてありますが、elseはカウントされません。

ただし、elseifはカウントされるので、気をつけてください。

例えば以下のような例だと...

if (A){
 // 1
} elseif(B) {
 // 2
} else {
 // 3
}

3通りに分岐して、1通りから3通りになるので、複雑さが+2ですね。

switchでのdefaultが加算されていない

さて、最後にswitch文のデフォルトが加算されていませんね。

これもちょっと例え話をしましょう。

最初メソッドが用意された時点で一本道が用意されます。(=複雑さ1)

これはメソッドがある時点で、メソッドを通る経路と通らない経路の二通りが生じるため、複雑さが+1されます。

そして、メソッドを通る道を歩いていると、そのまま看板が無いまっすぐな道(default)と、看板が立った分かれ道が三本(caseが三つ)ありました。

看板が無いまっすぐな道がdefaultで看板が立った道がcase 1, case 2, case 3です。

このとき、もともと一本だった道が分岐して4本(=複雑さが1から4)になります。

すると、複雑さはcaseの数だけ加算(+3)すれば良いですね。

つまるところ、defaultはもともとの一本道とおんなじですよ。ということです。

なので、普通はあり得ませんが、defaultしかないswitch文が仮に書かれていたとしても、そこでは分岐が生じない(必ずdefaultだけ通る)ため、複雑さは増えません。

言葉では難しいんですが...わかりましたでしょうか....?

まとめ

今回はPHP MDにおけるCyclomaticComplexityについて解説をしていきまいした!

このパラメータを簡潔に言い換えると...

"そのプログラムを最低何周すれば全ての経路を網羅できるかを表している指標"といってもいいかと思います。

最初に戻りますが、おそらく..."循環的"というのはこの何周循環すれば全ての経路を網羅できるか...ということにかかっているんでしょう。

また、できれば前述したのelse等がカウントされない理由も理解して欲しいですが、確認するためには最低限以下のことを数えれば良さそうです。

  • メソッドの数
  • if, elseifの数(elseのみは含まない)
  • while, forの数
  • switch文のcaseの数(defaultは数えない)

while,forもそこを通るか通らないかの二つに分岐していくため加算する必要がありますね。

必ず必須というわけでは無いですが、知っているとリファクタリングなどで結構便利になりそう...なきがします...!

できるだけこのCyclomaticComplexityを低く保てるように書いていくのも可読性もあがり(考えればいい処理の経路の数が減る)、メンテナンスもしやすくなるので、おすすめです!

うちは社内でphp mdを利用しているので、gitでmergeしようとすると怒られるんですけどね...orz

別の項目でも怒られたりしたらまた書きますね!

それでわ!

おすすめの記事