自分のプロジェクトで Code Climate を使ってみた時の話をします。
Code Climate とは?
- コードの品質とかを測れるサービス
- OSS なら無料で利用可能
使ってみた結果
私のコードに対して、下記のような指摘が来ました。
Function
toCommandSections
has 29 lines of code (exceeds 25 allowed).
(toCommandSections
メソッドが29行あるから、25行以下にしてくれ)
About 1 hr to fix
(1時間あれば直せる)
こちらは分かりやすい指摘です。
ただ"1時間で直せる"とは誰がどういう見解で言っているのか納得行きません。
Function
read
has a Cognitive Complexity of 8 (exceeds 5 allowed).
(read
メソッドの Cognitive Complexity が8だから、5以下にしてくれ)
About 45 mins to fix
(45分で直せる)
**Cognitive Complexity...?**一体なにを指摘されているのか分かりません。
しかも About 45 mins to fix と来ました。一体誰が?
Cognitive Complexity とは何か?
「コグニティブ・コンプレクシティー」、訳してみれば「認知的複雑度」でしょうか。
コードの複雑さを測る指標のひとつのようです。
ググると心理的の記事が多くヒットしますが、同名の用語と関係は無いようです。
「Cyclomatic Complexity」と「Cognitive Complexity」
Cognitive Complexity と似たものに**「Cyclomatic Complexity(サイクロマティック・コンプレクシティー、循環的複雑度)」**があります。こちらの方が一般によく知られていると思います。
Cyclomatic Complexity は以前から提唱されていましたが、その問題点を改善するための指標として Cognitive Complexity が考え出された、とのこと。
違いをまとめると、下記のような感じです。
Cyclomatic Complexity | Cognitive Complexity |
---|---|
サイクロマティック・コンプレクシティー | コグニティブ・コンプレクシティー |
循環的複雑度 | 認知的複雑度 |
1976年 Thomas J. McCabe 氏が考案1 | 2016年 SonarSource 社が考案2 |
機械的なテストの難しさを測る | 人間の理解の難しさを測る |
Cyclomatic Complexity と Cognitive Complexity の違いの例
たとえば下記2つのコードの、どちらが"複雑"だと思いますか?
// method1: switch文
function method1(n: number) {
switch (n) {
case 1:
console.log("ichi");
break;
case 2:
console.log("ni");
break;
case 3:
console.log("san");
break;
default:
console.log("ippai");
break;
}
}
// method2: ふたつの for 文のネスト
function method2(max: number) {
let total = 0;
labelA:
for (let i = 1; i <= max; i++) {
for (let j = 2; j < i; j++) {
if (i % j == 0) {
continue labelA;
}
}
total += 1;
}
return total;
}
「どちらが複雑か?」と言われれば、「前者の switch 文よりも、 後者の for と if のネストのほうが複雑」のように見えます。
しかし、Cyclomatic Complexity と Cognitive Complexity をそれぞれ算出すると、下記のようになります。
method | 内容 | Cyclomatic Complexity | Cognitive Complexity |
---|---|---|---|
method1 | switch文ひとつ | 4 | 1 |
method2 | forとifのネスト | 4 | 7 |
Cyclomatic Complexity は両方とも 4 で同値、つまりふたつのメソッドの複雑度は同じ、となりました。
一方 Cognitive Complexity は、前者は 1 、後者は 7 と明らかな差が付きました。 switch 文のほうが単純であるという事が、定量的に示されました。
switch 文ひとつであっても分岐網羅テストケースは 4 パターン必要になるため、 Cyclomatic complexity は 4 になります。しかしヒトが理解するには Switch 文のほうが明らかに単純ですので、 Cognitive Complexity の方は小さい値をとります。
Cognitive Complexity の実際の測り方
Cognitive Complexity は、基本的に下記3つのシンプルな法則に伴って算出されます。
コードの線形的な流れを乱すとき、複雑とみなす
Code is considered more complex for each "break in the linear flow of the code"
なにもないメソッドは、複雑度ゼロです。
// Cognitive Complexity: 0
function method1() {
}
そこに if
文や 例外の catch
のように、"コードの線形的な流れを乱す要素"が加わると、+1 されます
// Cognitive Complexity: 1
function method2() {
if (true) { // +1
}
}
// Cognitive Complexity: 1
function method3() {
try {
} catch (e) { // +1
}
}
組み合わせると、Complexity は増えていきます。
// Cognitive Complexity: 2
function method4() {
try {
if (true) { // +1
}
} catch (e) { // +1
}
}
// Cognitive Complexity: 2
function method5() {
if (true) { // +1
} else { // +1
}
}
他にもループや switch
文、論理演算子やラベルジャンプ等が加わると、複雑度が加算されていきます。
流れを乱すネストが深いほど、複雑とみなす
Code is considered more complex when "flow breaking structures are nested"
ここからが面白いところです。
if
などがネストされていると、ネストされたぶんだけ複雑度が加算されていきます。
たとえば下記のコード。
function method1() {
if (true) {
if (true) {
}
}
}
ひとつめの if
は複雑度 +1 です。
しかしふたつめの if
は、ひとつネストされた分が加算されて、複雑度 +2 となります。
つまり、合計の Cognitive Complexity は 1 + 2 = 3 になります。
// Cognitive Complexity: 3
function method1() {
if (true) { // +1
if (true) { // +2 (= if +1, nest +1)
}
}
}
そのため下記ふたつのメソッドを比べたときに、前者の「ネストされた if
」の方が、後者の「ネストされていない if
」よりも複雑であると、定量的に判断されます。
// Cognitive Complexity: 3 こちらのほうが複雑
function method1() {
if (true) { // +1
if (true) { // +2 (= if +1, nest +1)
}
}
}
// Cognitive Complexity: 2 こちらのほうが単純
function method2() {
if (true) { // +1
}
if (true) { // +1
}
}
ショートハンドを使って複数の文をひとつにまとめていれば、複雑さが高いとはみなされない
Code is not considered more complex when it uses shorthand that the language provides for collapsing multiple statements into one
たとえば下記のRubyコードは、 if
文と論理演算子 &&
がそれぞれ +1 加算され、複雑度は 2 になります。
# Cognitive Complexity: 2
def method1()
if obj && obj.func?
return true
end
false
end
ここで、Ruby の safe navigation(ぼっち演算子) &.
3 を使うと、論理演算子 &&
を減らすことができます。
このとき複雑度は 1 となります。
# Cognitive Complexity: 1
def method2()
if obj&.obj.func?
return true
end
false
end
ちなみに、当然下記のように if
も無くしてしまえば複雑度はゼロです。
# Cognitive Complexity: 0
def method3()
obj&.obj.func?
end
実際の指摘
冒頭に挙げた Code Climate で Cognitive Complexity を 8 から 5 にしろと指摘された例を見てみます。
// Cognitive Complexity: 8
function read(filepath: string, __: (key: string) => string): string {
try {
return fs.readFileSync(format(parse(filepath)), "utf8");
} catch (e) { // +1
if (e instanceof Error) { // +2 (= if +1, nest +1)
if (e.message.indexOf("ENOENT") === 0) { // +3 (= if +1, nest +2)
// tslint:disable-next-line:no-console
console.log(__("FileNotFound"));
} else { // +1
throw e;
}
} else { // +1
throw e;
}
}
return "";
}
try
~ catch
と if
で3重のネストになって複雑度が増えていた事がわかります。確かに読みづらいコードになっています。
Cognitive Complexity の使い方
ローカルで計測できるやつ作った
index.ts
や index.rb
を適当に編集して $ docker
コマンドを打つと、Cognitive Complexity を算出します。Windows は非対応なので Linux 環境で使ってください。
$ docker-compose run --rm analyze
Starting analysis
Running structure: Done!
== index.js (3 issues) ==
2-3: Function `method0` has a Cognitive Complexity of 0 (exceeds -1 allowed). Consider refactoring. [structure]
7-12: Function `method1` has a Cognitive Complexity of 2 (exceeds -1 allowed). Consider refactoring. [structure]
16-21: Function `method2` has a Cognitive Complexity of 3 (exceeds -1 allowed). Consider refactoring. [structure]
やっていることは、CodeClimate CLI の Docker イメージを持ってきて Cognitive Complexity だけ 許容値-1(ゼロ以上でアラート)で動かしているだけです。
まとめ
- ネストを減らして、分岐を減らして、複雑度の低いコードを書こう
- Cognitive Complexity を CI で測りたい?
- Code Climate を使おう
注意事項: NGな使い方
- 「お前のコード Cognitive Complexity 高すぎない?」と新人にコードレビューしてはいけません
- 新人が死にます 4
- 「俺なら45分で直せる」と新人にコードレビューしてはいけません
- 新人が死にます 4
参考文献
- Cognitive Complexity - Code Climate
- COGNITIVE COMPLEXITY - A new way of measuring understandability - G. Ann Campbell