Help us understand the problem. What is going on with this article?

Cognitive Complexity で、コードの読みやすさを定量的に計測しよう

自分のプロジェクトで Code Climate を使ってみた時の話をします。

Code Climate とは?

  • コードの品質とかを測れるサービス
  • OSS なら無料で利用可能

使ってみた結果

私のコードに対して、下記のような指摘が来ました。
image.png

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つのコードの、どちらが"複雑"だと思いますか?

typescript
// 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;
  }
}
typescript
// 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"

なにもないメソッドは、複雑度ゼロです。

typescript
// Cognitive Complexity: 0
function method1() {
}

そこに if 文や 例外の catch のように、"コードの線形的な流れを乱す要素"が加わると、+1 されます

typescript
// Cognitive Complexity: 1
function method2() {
  if (true) { // +1
  }
}
typescript
// Cognitive Complexity: 1
function method3() {
  try {
  } catch (e) { // +1
  }
}

組み合わせると、Complexity は増えていきます。

typescript
// 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 などがネストされていると、ネストされたぶんだけ複雑度が加算されていきます。

たとえば下記のコード。

typescript
function method1() {
  if (true) {
    if (true) {
    }
  }
}

ひとつめの if は複雑度 +1 です。
しかしふたつめの if は、ひとつネストされた分が加算されて、複雑度 +2 となります。

つまり、合計の Cognitive Complexity は 1 + 2 = 3 になります。

typescript
// Cognitive Complexity: 3
function method1() {
  if (true) {   // +1
    if (true) { // +2 (= if +1, nest +1)
    }
  }
}

そのため下記ふたつのメソッドを比べたときに、前者の「ネストされた if 」の方が、後者の「ネストされていない if 」よりも複雑であると、定量的に判断されます。

typescript
// 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 になります。

ruby
# Cognitive Complexity: 2
def method1()
  if obj && obj.func?
    return true
  end

  false
end

ここで、Ruby の safe navigation(ぼっち演算子) &. 3 を使うと、論理演算子 && を減らすことができます。
このとき複雑度は 1 となります。

ruby
# Cognitive Complexity: 1
def method2()
  if obj&.obj.func?
    return true
  end

  false
end

ちなみに、当然下記のように if も無くしてしまえば複雑度はゼロです。

ruby
# Cognitive Complexity: 0
def method3()
  obj&.obj.func?
end

実際の指摘

冒頭に挙げた Code Climate で Cognitive Complexity を 8 から 5 にしろと指摘された例を見てみます。

typescript
// 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 "";
}

trycatchif で3重のネストになって複雑度が増えていた事がわかります。確かに読みづらいコードになっています。

Cognitive Complexity の使い方

ローカルで計測できるやつ作った

https://github.com/s2terminal/cognitive-complexity-example

index.tsindex.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 CLIDocker イメージを持ってきて Cognitive Complexity だけ 許容値-1(ゼロ以上でアラート)で動かしているだけです。

まとめ

  • ネストを減らして、分岐を減らして、複雑度の低いコードを書こう
  • Cognitive Complexity を CI で測りたい?

注意事項: NGな使い方

  • 「お前のコード Cognitive Complexity 高すぎない?」と新人にコードレビューしてはいけません
    • 新人が死にます 4
  • 「俺なら45分で直せる」と新人にコードレビューしてはいけません
    • 新人が死にます 4

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした