COGNITIVE COMPLEXITY
COGITIVE:認識の
開発者が直感的に感じる複雑さを表す新しい測定値。
メソッドの理解可能性をより正確に図るための指標。
以下は上記をGoogle翻訳したもの
概要
循環的複雑度は、当初、モジュールの制御フローの「テスト可能性と保守性」の測定値として定式化されました。 前者の測定には優れていますが、基礎となる数学的モデルでは、後者を測定する値を生成するには不十分です。 このホワイトペーパーでは、循環的複雑度の欠点を修正し、メソッド、クラス、およびアプリケーションの維持の相対的な難しさをより正確に反映する測定値を生成するために、数学モデルを使用してコードを評価することから脱却する新しいメトリックについて説明します。
用語に関する注記
Cognitive Complexityは、ファイルやクラス、メソッド、プロシージャ、関数などに等しく適用される言語に依存しないメトリックですが、オブジェクト指向の用語「クラス」と「メソッド」は便宜上使用されています。
前書き
Thomas J.McCabeの循環的複雑度は長い間メソッドの制御フローの複雑度を測定するための事実上の標準でした。 もともとは「テストや保守が難しいソフトウェアモジュールを特定する」ことを目的としていましたが、メソッドを完全にカバーするために必要なテストケースの最小数を正確に計算しますが、理解しやすさの十分な尺度ではありません。 これは、循環的複雑度が等しい方法では、必ずしもメンテナに同じ難易度が与えられるとは限らず、一部の構造を過大評価し、他の構造を過小評価することで、測定が「オオカミを叫ぶ」という感覚につながるためです。
同時に、循環的複雑度はもはや包括的ではありません。 1976年にFortran環境で作成されたもので、try / catchやラムダなどの最新の言語構造は含まれていません。
そして最後に、各メソッドの最小循環的複雑度スコアは1であるため、循環的複雑度の総計が高い特定のクラスが、大きくて保守が容易なドメインクラスなのか、複雑な制御フローを持つ小さなクラスなのかを知ることはできません。 クラスレベルを超えて、アプリケーションの循環的複雑度スコアがコードの合計行に相関することは広く認識されています。 言い換えれば、循環的複雑度はメソッドレベル以上ではほとんど役に立ちません。
これらの問題の解決策として、Cognitive Complexityは、現代の言語構造に対処し、クラスおよびアプリケーションレベルで意味のある値を生成するように策定されています。 さらに重要なことは、数学モデルに基づいてコードを評価する慣行とは異なり、制御フローを理解するために必要な精神的または認知的努力に関するプログラマーの直感に対応する制御フローの評価をもたらすことができることです。
問題の実例
対処するように設計されている問題の例から、認知の複雑さの議論を始めることは有用です。 次の2つの方法は、循環的複雑度は同じですが、理解しやすさの点で著しく異なります。
code: サンプル1
int sumOfPrimes(int max) { // +1
int total = 0;
OUT: for (int i = 1; i <= max; ++i) { // +1
for (int j = 2; j < i; ++j) { // +1
if (i % j == 0) { // +1
continue OUT;
}
}
total += i;
}
return total;
} // Cyclomatic Complexity 4
code:サンプル2
String getWords(int number) { // +1
switch (number) {
case 1: // +1
return "one";
case 2: // +1
return "a couple";
case 3: // +1
return “a few”;
default:
return "lots";
}
} // Cyclomatic Complexity 4
循環的複雑度の基礎となる数学的モデルは、これら2つのメソッドに等しい重みを与えますが、sumOfPrimesの制御フローがgetWordsの制御フローよりも理解しにくいことは直感的に明らかです。 これが、Cognitive Complexityが制御フローを評価するための数学的モデルの使用を放棄し、プログラマーの直感を数値に変換するための一連の単純なルールを支持する理由です。
基本的な基準と方法論
認知の複雑さのスコアは、次の3つの基本的なルールに従って評価されます。
1.複数のステートメントを読みやすく1つに短縮できる構造を無視します
2.コードの線形フローのブレークごとにインクリメント(1を追加)します
3.フローブレーク構造がネストされている場合のインクリメント
さらに、複雑さスコアは4つの異なるタイプの増分で構成されます。
A.ネスティング-相互にネストされた制御フロー構造について評価
B.構造-入れ子の増分の対象であり、入れ子の数を増やす制御フロー構造で評価されます
C.基本-ネストの増分の対象とならないステートメントで評価
D.ハイブリッド-入れ子の増分の対象ではないが、入れ子の数を増やす制御フロー構造で評価
増分のタイプは数学に違いはありませんが(増分ごとに最終スコアに1が加算されます)、カウントされる機能のカテゴリを区別することで、ネストの増分が適用される場所と適用されない場所を簡単に理解できます。
これらのルールとその背後にある原則については、次のセクションでさらに詳しく説明します。
速記を無視する
認知的複雑性の定式化における指針となる原則は、それが優れたコーディング慣行を奨励するべきであるということでした。 つまり、コードを読みやすくする機能を無視するか、割引する必要があります。
メソッド構造自体が代表的な例です。 コードをメソッドに分割すると、複数のステートメントを1つの、喚起的に名前が付けられた呼び出しに凝縮できます。つまり、「短縮」できます。 したがって、認知の複雑さはメソッドに対して増加しません
Cognitive Complexityは、多くの言語で見られるnull合体演算子も無視します。これも、複数行のコードを1つに短縮できるためです。 たとえば、次のコードサンプルはどちらも同じことを行います。
code:サンプル1
MyObj myObj = null;
if (a != null) {
myObj = a.myObj;
}
code:サンプル2
MyObj myObj = a?.myObj;
左側のバージョンの意味は処理に少し時間がかかりますが、右側のバージョンは、null合体構文を理解するとすぐに明確になります。 そのため、CognitiveComplexityはnull合体演算子を無視します。
線形流れの中断の増分
COGNITIVE COMPLEXITYの定式化におけるもう1つの指針は、コードの通常の線形フローを上から下、左から右に分割する構造では、メンテナがそのコードを理解するために一生懸命働く必要があるということです。 この余分な努力を認めて、認知の複雑さは以下の構造的増分を評価します。
● Loop structures: for, while, do while, ...
● Conditionals: ternary operators, if, #if, #ifdef, ... 以下のハイブリッド増分を評価します。
● else if, elif, else, …
ifを読み取るときに精神的なコストがすでに支払われているため、これらの構造のネストの増分は評価されません。
これらの増分ターゲットは、循環的複雑度に慣れている人にはおなじみのようです。 さらに、認知の複雑さは次の場合にも増加します。
Catches
キャッチは、ifと同じように、制御フロー内の一種の分岐を表します。 したがって、各catch句は、認知の複雑さを構造的に増加させます。 キャッチされた例外タイプの数に関係なく、キャッチは認知的複雑性スコアに1ポイントしか追加しないことに注意してください。 tryとfinallyブロックは完全に無視されます。
Switches
スイッチとそのすべてのケースを組み合わせると、単一の構造的増分が発生します。
循環的複雑度では、スイッチはif-elseifチェーンのアナログとして扱われます。 つまり、スイッチの各ケースは、制御フローの数学モデルに分岐を引き起こすため、増分を引き起こします。
しかし、メンテナの観点からは、スイッチ(単一の変数を明示的に名前が付けられたリテラル値のセットと比較する)は、if-else ifチェーンよりもはるかに理解しやすいです。後者は、任意の数を使用して任意の数の変数と値の比較を行う可能性があるためです。
つまり、if-else ifチェーンを注意深く読む必要がありますが、スイッチは一目でわかることがよくあります。
論理演算子のシーケンス
同様の理由で、認知の複雑さは、バイナリ論理演算子ごとに増加しません。 代わりに、バイナリ論理演算子の各シーケンスの基本的な増分を評価します。 たとえば、次のペアについて考えてみます。
a && b
a && b && c && d
a || b
a || b || c || d
各ペアの2行目を理解することは、最初の行を理解することほど難しくはありません。 一方、次の2行を理解するための努力には著しい違いがあります。
a && b && c && d
a || b && c || d
ブール式は、混合演算子を使用すると理解が難しくなるため、同様の演算子の新しいシーケンスごとに認知の複雑さが増します。 例えば:
code:サンプル1
if (a // +1 for if
&& b && c // +1
|| d || e // +1
&& f) // +1
code:サンプル2
if (a // +1 for if
&& // +1
!(b && c)) // +1
Cognitive Complexityは、循環的複雑度に比べて同様の演算子に「割引」を提供しますが、変数の代入、メソッドの呼び出し、returnステートメントなどのバイナリブール演算子のすべてのシーケンスに対してインクリメントします。
再帰
循環的複雑度とは異なり、認知的複雑度は、直接的か間接的かを問わず、再帰サイクルの各メソッドに基本的な増分を追加します。 この決定には2つの動機があります。 まず、再帰は一種の「メタループ」を表し、認知の複雑さはループに対して増加します。 第二に、認知の複雑さは、メソッドの制御フローを理解することの相対的な難しさを推定することであり、一部の熟練したプログラマーでさえ、再帰を理解するのが難しいと感じています。
Jumps to labels
goto,label,break, continueは認知の複雑さに基本的な増分が追加されます。 ただし、早期に戻るとコードがより明確になることが多いため、他のジャンプや早期終了によってインクリメントが発生することはありません。
ネストされたフローブレーク構造の増分
各シリーズの実行パスの数に関係なく、5つのifおよびfor構造の線形シリーズは、同じ5つの構造が連続してネストされるよりも理解しやすいことは直感的に明らかです。 このような入れ子は、コードを理解するための精神的な要求を高めるため、CognitiveComplexityはその入れ子の増分を評価します。
具体的には、構造またはハイブリッド増分を引き起こす構造が別のそのような構造内にネストされるたびに、ネストのレベルごとにネスト増分が追加されます。 たとえば、次の例では、どちらの構造も構造増分またはハイブリッド増分のいずれにもならないため、メソッド自体またはtryのネスト増分はありません。
code:サンプル
void myMethod () {
try {
if (condition1) { // +1
for (int i = 0; i < 10; i++) { // +2 (nesting=1)
while (condition2) { … } // +3 (nesting=2)
}
}
} catch (ExcepType1 | ExcepType2 e) { // +1
if (condition2) { … } // +2 (nesting=1)
}
} // Cognitive Complexity 9
ただし、if、for、while、およびcatch構造はすべて、構造とネストの両方の増分の影響を受けます。
さらに、トップレベルのメソッドは無視され、ラムダ、ネストされたメソッド、および同様の機能の構造的なインクリメントはありませんが、そのようなメソッドは、他のメソッドのような構造内にネストされたときにネストレベルをインクリメントします。
code:サンプル
void myMethod2 () {
Runnable r = () -> { // +0 (but nesting level is now 1)
if (condition1) { … } // +2 (nesting=1)
};
} // Cognitive Complexity 2
void myMethod2 () { // +0 (nesting level is still 0)
Runnable r = () -> { // +0 (but nesting level is now 1)
if (condition1) { … } // +3 (nesting=2)
};
} // Cognitive Complexity 4
影響
認知の複雑さは、メソッドの相対的な理解可能性をより正確に反映するメソッドスコアを計算することを主な目標とし、最新の言語構造に対処し、メソッドレベルより上で価値のあるメトリックを生成することを副次的な目標として策定されました。 明らかに、現代の言語構造に取り組むという目標は達成されました。 他の2つの目標は以下で検討されます
直感的に「正しい」複雑さのスコア
この議論は、循環的複雑度は等しいが理解しやすさが明らかに等しくない2つの方法から始まりました。 次に、これらの方法を再検討し、認知的複雑性スコアを計算します。
code:サンプル
int sumOfPrimes(int max) {
int total = 0;
OUT: for (int i = 1; i <= max; ++i) { // +1
for (int j = 2; j < i; ++j) { // +2
if (i % j == 0) { // +3
continue OUT; // +1
}
}
total += i;
}
return total;
} // Cognitive Complexity 7
code:サンプル2
String getWords(int number) {
switch (number) { // +1
case 1:
return "one";
case 2:
return "a couple";
case 3:
return “a few”;
default:
return "lots";
}
} // Cognitive Complexity 1
Cognitive Complexityアルゴリズムは、これら2つの方法に著しく異なるスコアを与えます。これらの方法は、相対的な理解可能性をはるかに反映しています。
メソッドレベルより上で価値のあるメトリック
さらに、メソッド構造の認知的複雑性は増加しないため、集計数が役立ちます。 これで、ドメインクラス(単純なゲッターとセッターが多数あるドメインクラス)と、メトリック値を比較するだけで複雑な制御フローを含むドメインクラスの違いがわかります。 したがって、認知の複雑さは、クラスとアプリケーションの相対的な理解可能性を測定するためのツールになります。
結論
コードの記述と保守のプロセスは人間のプロセスです。 それらの出力は数学モデルに準拠する必要がありますが、数学モデル自体には適合しません。 これが、数学モデルが必要な労力を評価するには不十分である理由です。
認知の複雑さは、ソフトウェアの保守性を評価するために数学的モデルを使用する慣行から脱却します。 これは、循環的複雑度によって設定された前例から始まりますが、人間の判断を使用して、構造をカウントする方法を評価し、モデル全体に何を追加するかを決定します。 その結果、以前のモデルで利用可能であったよりも理解しやすさのより公正な相対的評価としてプログラマーを襲うメソッドの複雑さのスコアが得られます。 さらに、Cognitive Complexityはメソッドの「エントリのコスト」を請求しないため、メソッドレベルだけでなく、クラスおよびアプリケーションレベルでもより公平な相対評価を生成します。