見出し画像

gocycloを使ってgo言語のプロダクトをシンプルに維持する

1. 対象者
- go言語を使用している方
- 対象のプロダクトの可読性が悪いと感じている場合

2. 概要
ソフトウェアの品質を一定の水準に維持する方法には大きく、テストケースの網羅率を測定する方法とソフトウェアメトリクス(モジュールの依存度、ソースコードの行数、ネストの深さ、循環的複雑度.. etc)を測定する方法があります。今回は、これらソフトウェアメトリクスの内、循環的複雑度に着目してgo言語で実践する方法を紹介していきます。(go言語を取り扱いますが、概念自体は技術を制限するものでは無いので、他のプロダクトでも応用可能です。)

3. 循環的複雑度
循環的複雑度は、対象の関数やメソッドがどれだけ複雑さを持っているかを表す数値です。この循環的複雑度が高ければ高いほど、可読性が低く保守性が悪いコードである可能性が高くなります。 使用する技術によって、目安は変わってきますが、おおよそ20~30以内におさめるのが良いとしていることが多いようです。次に、具体的に複雑度の定義を説明します。

4. 循環的複雑度の定義
実際にどのように循環的複雑度を計算するのかについて説明していきます。計算対象のプログラムに対して、制御フローグラフ(control flow graph) を書き出して”グラフのエッジ数”、”グラフのノード数”、”連結されたコンポーネント数を求め、以下の循環的複雑度Mの式に代入します。

M = E − N + 2P
M = 循環的複雑度
E = グラフのエッジ数
N = グラフのノード数
P = 連結されたコンポーネントの数

実際の例を擬似言語を題材に上げ、循環的複雑度Mを求めてみます。条件はcondition、処理はstatement, 開始と終了はそれぞれstartとendと記述するものとします。

start;
statement 1;
if ( condition1 ) {
    statement 2;
} else {
    statement 3;
}
statement 4;
statement 5;
if ( condition2 ) {
    statement 6;
} else {
    statement 7;
}
statement 8;
end;

この処理を制御フローグラフ(control flow graph)に置き換えると以下のようなグラフに変換できます。(開始と終了はstartノード, endノードとして青色で示しています。またグレーはstatementノードを表します) if elseが2回含まれていますので、それぞれstatement1とstatement5のタイミングで2パターンに分岐します。この図でグラフのエッジ(補足1)を数え上げると11になります。 また全てのノードを数えると10です。連結されたコンポーネント(後述)は1になるので、E, N, Pはそれぞれ11,10,1となり循環的複雑度 M = 11 - 10 + 2 * 1 = 3  と計算されます。

[補足1] 連結されたコンポーネントとは
モジュール(サブルーチン、プロシージャ、その他)を分岐点数として扱う場合、制御フローグラフでは独立したコンポーネントとしてみなされます。以下の場合は3つコンポーネントが存在することになりますのでP=3です。

5. 循環的複雑度の定義(2)
複雑度を求めるもう一つの方法として、startとendを繋いだ制御フローグラフに対して、閉じたループを数え上げる方法があります。以下の図は、先に示したプログラムの制御フローグラフですが、ループを数え上げると同じように3になることがわかります。

M = e − n + p
e = 制御グラフ内のエッジ数
n = 制御グラフ内のノード数
p = 連結されたコンポーネントの数


6. gocyclo
go言語のライブラリ(gocyclo)を使用して、循環的複雑度を求めて行きます。

// インストール方法
go get github.com/fzipp/gocyclo

6.1. if-else単純分岐

func isOddNumber (i int64) bool {
	if i%2 == 1 {
		return true
	} else {
		return false
	}
}

一番左側の数値が循環的複雑度になります。このケースだと2です。

2 main isOddNumber hoge.go:4:1

6.2. switch

func getMonthName(month int) string {
	switch month {
	case 1:
		return "January"
	case 2:
		return "February"
	case 3:
		return "March"
	case 4:
		return "April"
	case 5:
		return "May"
	case 6:
		return "June"
	case 7:
		return "July"
	case 8:
		return "August"
	case 9:
		return "September"
	case 10:
		return "October"
	case 11:
		return "November"
	case 12:
		return "December"
	default:
		return ""
	}
}

defaultと12個のcaseなので結果は14となります。見にくくは無いですが、mapを使えば分岐は2に抑えられそうです。

14 main getMonthName hoge.go:13:1

7. [番外編] 
googleが提供しているツールでbugspotというものがあります。こちらは、githubのcommitメッセージによくfixやclosedが含まれている場合は、バグを含むコードが多いという経験則に基づくロジックによって危険なコードをランキング形式で表示することができます。 筆者のプロジェクトでこれを利用した結果、循環的複雑度が高いコードとbugspotが報告するコードが正の相関を持つことがわかりました。つまりこれは、循環的複雑度が高いコードほどバグfixのcommitを行なっている頻度が高いということを裏付けています。
http://www.publickey1.jp/blog/11/post_193.html
循環的複雑度を低く保つように目標を設定するに当たって定量的な評価方法が必要ならばこちらもセットで導入すると良いかもしれません。

8. まとめ

循環的複雑度のコンセプトと、計算方法の説明、実際のツールを使用してgolangで計測する方法までを示しました。自分が担当しているプロジェクトで計測してみて、20〜30を超えるようなヘビーな関数やメソッドが多くあるようならば、リファクタリングの機会を設けたりチームでルール付けしてみてはいかがでしょうか。

この記事が気に入ったらサポートをしてみませんか?