2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

McCabe の循環的複雑度(McCabe Cyclomatic Complexity : 以下、MCC)

Last updated at Posted at 2021-07-13

#MCC を python で使う場合
python で計測したい時はここここにインストール方法や利用方法が書いてあるので参考にしてほしい。

#MCC の概要
とても簡単で、条件分岐がいくつ存在していますか?というメトリクスである。この数値を参考にすれば、テスト項目が最低限何個必要かをざっくりと知ることができるので非常に便利である。なお、条件分岐がない場合、動作テストは最低限必要=1 として数えることになる。

#MCC で Why を考える
ここで早速 5W1H のフレームワークの Why を使ってみよう。
Why should we use MCC? -> Because...
ということで答えを言ってしまえば、テスト漏れの発生/可読性の低下/保守性の低下を事前に予期できるからである。
その派生的観点として、プログラムの、特に関数分割の指針として利用できる。なぜなら、条件分岐が異常に多い => そこには複数の機能が混ざり合っている可能性が高いからである。これについては、発展部分で関数化した整ったプログラムとその状況を破壊したプログラムを比較できるようにしているので、ぜひ感じてほしい。

Why could we expect it?
ということで、まずはテスト漏れの発生について考えてみよう。条件分岐が多ければ当然のことながらテストすべきパターン数が増える。これは組み合わせ論が苦手な人でも容易に想像できるだろう。条件分岐が増えれば増えるほど、テスト項目の不足、テスト時のミス等のヒューマンエラーの発生確率を押し上げてしまう。
改造時には旅に出たくなるようなテスト項目に襲われることになるだろう。

次に可読性の低下/保守性の低下について考えてみよう。(これらは大抵の場合、セットで考えることになる。)
あなたの目の前に一つの関数に大量の if 文や for 文が混ざっているプログラムが転がっている。あなたは上司の業務依頼により、これを改造しなければいけなくなった。そのようなプログラムをあなたはすぐに理解できますか?改造する勇気はありますか?大抵は No だろう。このときあなたが感じている直感は、
  読んでもわかりにくい = 可読性が低い
  改造が非常に困難 = 保守性が低い
ということになる。それを改造すれば、更なる可読性/保守性の低下を生じるだろう。

##ここまでのまとめ

MCC を用いると、以下のような利点がある。

What could we know/use?

  1. 最低限必要な直感的なテスト項目数がわかる。
    -> 数値の境限界パターン等でテスト項目が N 倍化、N 乗化することはよくあるのであくまで参考に。

  2. 値が異常に高い場合、関数化等の機能分割の判断材料として利用できる。
    -> GUI で大量のチェックボックスがある場合等の極端なケースは仕方がない場合がある。
    -> 値が高い = 良くない、ではない。ただし、その可能性は高いと考えること。

  3. 値が高い = 読解しにくい = 可読性が低いと考える判断材料となる。

  4. 値が高い = 改造しにくい = 保守性が低いと考える判断材料となる。

#MCC の例
では、いくつか例を使って理解を深めてみよう。

以下は MCC = 1 の例
条件分岐無し、関数は条件分岐とは考えない

a.py
def func():
    x=2

以下は MCC = 2 の例
条件分岐が1つ

b.py
def func():
    x=2

    if(x == 1):
        print('Value is %d' % (x))

こちらも MCC = 2 の例
for 文は条件分岐が入っているので条件分岐が1つと数える
while 文も同様

b.py
def func():
    for i in range(0, 10):
        print(i)

以下は MCC = 3 の例
条件分岐が2つ、if-else、if の連続を使うかは関係ない

b.py
def func():
    x=2

    if(x == 1):
        print('Value is %d' % (x))
    elif(x == 2):
        print('Value is %d' % (x))

こちらも MCC = 3 の例
条件分岐が2つ、if の連続

c.py
def func():
    x=2

    if(x == 1):
        print('Value is %d' % (x))
    if(x == 2):
        print('Value is %d' % (x))

こちらも MCC = 3 の例
条件分岐が2つ、if がネストしている、深さは関係ない

d.py
def func():
    x=2

    if(x > 0):
        print('Value is %d, x > 0' % (x))

        if(x < 2):
            print('Value is %d, x < 2' % (x))

#実践例(できるだけ機能毎に関数化したケース)

どこにでもあるような二次関数の解を求めるプログラムを例に考えてみよう。
なお、radon の仕様上、main 処理部分の MCC 計測のために関数化する必要があるので注意が必要である。

sample_mcc1.py
#!/usr/bin/env python                                                                                                                                                                           
# -- coding:utf8 --                                                                                                                                                                             


import sys
import math
import re

# 二次関数の解を求める
def solution(a, b, c):
    # 判別式
    discriminant = b*b-4*a*c
    if  (discriminant > 0):
        tmp = math.sqrt(discriminant)
        ans = [(-b+tmp)/2*a, (-b-tmp)/2*a]
        print('Solution are [%.3f], [%.3f]' % (ans[0], ans[1]))
    elif(discriminant == 0):
        ans = -b/(2*a)
        print('Solution is multople root [%.3f]' % (ans))
    elif(discriminant < 0):
        print('No solution, because of discriminant < 0. : [%.3f]' % (discriminant))

# 引数が正負の数であるかを判断する
def isnumber(str):
    return re.sub('^-', '', str).isdecimal()

# main 処理
def main(args):
    if(len(args) < 4):
        print('input 3 values: ./sample_mcc1.py val1 val2 val3')
        exit(0)

    for str in args[1:]:
        if(isnumber(str) == False):
            print('%s is not value' % (str) )
            exit(0)

    [a, b, c] = [float(args[1]), float(args[2]), float(args[3])]
    print('Let\'s caliculate the solutions for ax^2+bx+c -->  %dx^2+%dx+%d.' % (a, b, c))

    solution(a, b, c)


##### main #####                                                                                                                                                                                
main(sys.argv)

試しに MCC を計測すると、

radon cc -s sample_mcc1.py
sample_mcc1.py
    F 10:0 solution - A (4)
    F 27:0 main - A (4)
    F 23:0 isnumber - A (1)

と、いずれも A 評価で MCC = 1 or 4 という結果となっているので問題はなさそうである。
ここで少し復讐をしてみよう。

MCC を利用すれば、以下のことを考える材料となることをそれとなく述べてきた。
(1)MCC の値がテストで最低限必要な数を表していることが直感的に見て取れること
※isnumber は正負両方あるので実際は2パターン必要なことは注意
(2)プログラムの読みやすさは高いか?(可読性)
(3)プログラムは改造しやすそうか?(保守性)

おそらく、(1)〜(3)のいずれも Yes だろう。

また機能毎に関数化できているため、
 改造後のテストが変更した関数のみで OK の可能性があったり
 テスト項目の漏れ等のヒューマンエラー回避に繋がったり
結果的に作業コスト削減につながりそうである。
こういったことに気が付けるようになればプログラマーとして成長していると考えていいだろう。

では、これらを破壊するようにプログラムを変えてみよう。

#実践例(とりあえず作ってみたレベル)

全く同じ動作をするプログラムを関数化せずに書くと以下のようになる。

sample_mcc2.py
#!/usr/bin/env python                                                                                                                                                                           
# -- coding:utf8 --                                                                                                                                                                             


import sys
import math
import re

# main 処理
def main(args):
    if(len(args) < 4):
        print('input 3 values: ./sample_mcc1.py val1 val2 val3')
        exit(0)

    for str in args[1:]:
        # 引数が正負の数であるかを判断する
        if(re.sub('^-', '', str).isdecimal() == False):
            print('%s is not value' % (str) )
            exit(0)

    [a, b, c] = [float(args[1]), float(args[2]), float(args[3])]
    print('Let\'s caliculate the solutions for ax^2+bx+c -->  %dx^2+%dx+%d.' % (a, b, c))

    # 二次関数の解を求める
    # 判別式
    discriminant = b*b-4*a*c
    if  (discriminant > 0) :
        tmp = math.sqrt(discriminant)
        ans = [(-b+tmp)/2*a, (-b-tmp)/2*a]
        print('Solution are [%.3f], [%.3f]' % (ans[0], ans[1]))
    elif(discriminant == 0):
        ans = -b/(2*a)
        print('Solution is multople root [%.3f]' % (ans))
    elif(discriminant < 0) :
        print('No solution, because of discriminant < 0. : [%.3f]' % (discriminant))


##### main #####                                                                                                                                                                                
main(sys.argv)

単純なプログラムだから別に問題ないと思うかも知れないが、アプリ開発でこんなことをしたらどうなるか...
想像できる方には合掌しつつ、MCC を計測してみよう。

radon cc -s sample_mcc2.py
sample_mcc2.py
    F 10:0 main - B (7)

と、B 評価に降格し MCC = 7 という結果となっている。

MCC の値が多い => 条件分岐が多い => 機能が混ざっている可能性が高い、を感じられるだろうか?
もしくは、お、さっきより MCC の合計値が減っているからテスト項目が少なくなるじゃないか!関数なんか不要じゃないか!と思っただろうか?
後者の認識の方は、一箇所修正したら毎回同じテスト項目+αを実施しなければ、このプログラムの信頼性が確保できないことを早く認識すべきです。

#問題

##問題1
実践例(とりあえず作ってみたレベル)のプログラムで満足している方は、引数の個数に応じて一次〜四次関数の解を求めるプログラムを開発してみましょう。MCC の利用価値に気がついたり、可読性や保守性を考えることの大切さ、そして関数化の素晴らしさを感じると思います。
なお、三次関数は解の公式がすごいことになっているので、三次関数は無理!と表示するで OK です。
余裕があれば、五次関数以上は解なし!と表示するようにもしてみましょう。
# 参考までに、一次関数は2つ、二次関数は3つ、三次関数は4つ、四次関数は5つの引数を受け取ったときに解をそれぞれ求めるようにしてください。
# クラスも楽勝で理解している!という方は是非使って解決してください。

##問題2
それでも頑なに(某拳法伝承者の如く)私に関数などいらぬ!という方は、問題1で作ったプログラムやこれまで作ったプログラムを人に見せてみてください。客観的な感想をくれるはずです。

##問題3
これまであなたが作成してきた MCC の数値が高く、機能毎に関数に分割できると思われるプログラムを書き直し、ブラッシュアップして友達に見せてみよう!

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?