サイクロマティック複雑度:測定できても読みやすいとは限らない理由
要約
サイクロマティック複雑度はコードの分岐数を数える指標で、広く知られている「10」という閾値はNIST統計に基づいています。しかし複雑度が低いからといって読みやすいとは限らず、新人エンジニアのオンボーディングコストに大きな影響を与えます。この記事では、この指標を正しく理解し、チーム内で効果的に運用する方法を紹介します。
サイクロマティック複雑度とは、関数を通過する独立したパスの数を数える指標です。分岐がない関数はスコア1。ifを1つ追加すれば2。4つのcaseを持つswitchがあれば6へ跳ね上がります。この数値は、すべてのパスをカバーするのに必要なテストケースの数を示し、その関数を理解するのに必要な時間と相関します。
大切なのは、チームがこの指標をどう使うか、そしてどこで失敗するかです。
実際に測定されるもの
1976年にMcCabeが定義したサイクロマティック複雑度は、グラフ理論では「辺の数 - ノード数 + 2」です。しかし実務では理論不要です。決定ポイントを数えるだけです:if、else if、case、while、for、catch、&&、||、三項演算子。そして1を足します。それがスコアです。
def price(order):
if order.is_vip: # +1
if order.total > 100: # +1
return order.total * 0.8
return order.total * 0.9
elif order.has_coupon: # +1
return order.total * 0.95
return order.totalこのコードはスコア4です。高くはありませんが、「割引を適用する」というシンプルな機能のために既に3段階の分岐があります。パターンは明らかです:複雑度は1つのelifずつ忍び寄り、誰も悪役ではありません。
「10」という閾値の由来、そしてツール間の不一致
誰もが引用する数字は10です。それはNIST Special Publication 500-235に由来します。McCabeとWatsonが、10を超える上限は経験豊富なスタッフ、正式な設計、包括的なテスト計画を持つチーム向けとしていました。つまり:10は法則ではなく、デフォルトです。
ツール間では一致していません。ゲートを設定する前に知っておく価値があります:
NIST SP 500-235:10
ESLint
complexityルール:20Microsoft CA1502:25
Steve McConnell『Code Complete』:0-5は問題なし、6-10は注視、10以上はリファクタリング
Carnegie Mellon参考範囲:1-10は単純、11-20はテストが難しい、20以上は把握困難、50以上は保守不可能
4つの出所、4つの数字です。あなたのCIゲートが10で失敗し、同僚のサイドプロジェクトが25でゲートするなら、どちらが間違っているわけではありません。リスク許容度が異なるだけです。マイクロソフトの公式ドキュメントには、NIST推論の詳細が記されています。
実際の設定方針:新規コードは10-15を警告ラインとし、20でプルリクエストが追加レビューの対象となり、50は強制停止です。10以下なら、レビュー時間を費やす価値はありません。
落とし穴:低スコアでも読みにくい関数
ここでチームは失敗します。関数がスコア6でも本当に読みにくいことがあり、スコア14でも些細なことがあります。
10個のcaseを持つ平坦なswitchを考えてください。各caseが定数を返すなら、複雑度は10。ほとんどのエンジニアは15秒で読み終わります。なぜなら、パターンが明らかだから:1つが入って1つが出て、分岐間で状態は持ち越されません。次に、3段階のifネストとループ内で共有変数を変更する関数を考えてください。スコアは6かもしれませんが、どの分岐を本当に実行しているか知るにはコール・スタック全体を保持する必要があり、シニアエンジニアでも5分かかります。
サイクロマティック複雑度はパスを測定します。ネスト深度、変数スコープ、条件とその効果がファイル内でどれだけ離れているかは測定しません。同じスコアの2つの関数は、まったく異なる読み方になることがあります。
「10未満は対応可能」という代理指標として扱い、linterが緑だからプルリクエストを通し、3週間後に新人エンジニアが同じ関数の中で午後を失うのを見たチームは多いです。スコアは合格した。でも読みやすさは向上していません。
サイクロマティック複雑度 vs 認知複雑度:異なる質問
SonarSourceは認知複雑度を2017年に導入し、このギャップを埋めることにしました。サイクロマティック複雑度は「この関数は何個のパスを持つか」と答えます。認知複雑度は「この関数を頭の中に保つのはどれだけ難しいか」と答えます。ネスト深度に罰則を与えることで。
switch文は認知スコアをほとんど動かしません。ループ内の3段階ifは素早く動きます。なぜなら、ネストの各レベルが前のものの精神的コストを複合化させるから。SonarSourceの解説には、自分で実装したい場合のスコアリングルールが書かれており、複雑度をサポートするほとんどのlinterは、別の独立したルールとして認知複雑度もサポートしています。
チームが小さく、誰もが既に複雑な関数の場所を知っているなら、認知複雑度は無視してください。2人以上が自分で書いていないコードをレビューしている瞬間に有効にしてください。それはちょうどこのギャップが対応するためにある場面です。

AIコードレビューツール:何ができて何ができない
GitHub Copilot、CodeRabbit、Qodo、Greptileは全て、ある程度の複雑度シグナルをプルリクエストに表示します。あるツールは閾値を超えた関数にフラグを立てます。あるツールは「このPRは3ファイルの複雑度を増加させた」と要約します。ほとんどが、6ヶ月後にコンテキストなしでファイルを開く人にとってなぜそれが重要なのかを説明しません。
それが実際のギャップです。PRの複雑度警告はコメント内の数字です。追加された分岐がそこにあるべきか、3つのファイルに渡ってロジックが重複していないか、正しい修正が守備節句かメソッド抽出かを教えてくれません。AIツールは数えるのが得意です。でも、混乱の形を説明するのはまだ苦手です。
実際に隙間を埋めるのは、数値を人間がまだ答える必要がある質問と組み合わせることです:この関数は1つのことをするのか、それともif文でラップされた3つのことをするのか?linterはそれに答えません。ただ、どこを見るかを教えてくれるだけです。
100K行のリポジトリで100行のプロジェクトより重要な理由
自分で書いたコードベースでは、複雑度は既に解いた記憶問題です。10個の複雑な関数を名前で知っていて、なぜ複雑なのかを知り、考えなしにそれを回避します。しかし、このオーディエンスのほとんどはそういう状況ではありません。
5人から50人のエンジニアが共有するリポジトリでは、誰も完全な地図を保持していません。スコア22で元の著者が完璧に理解していた関数が、6ヶ月後には、別のエンジニアが1から再構築する必要のある関数になります。通常、締め切りの下で。既にやったことがあります:関数名でgrep、ファイル内でCtrl+F、疑わしい行をgit blame、チームを去った8ヶ月前の誰かにメッセージ。
これは複雑度が検索と相互作用し始める場所でもあります。高複雑度の関数は正確に要約するのが難しく、つまり、チームメイトやコード検索ツールが1文で正確に説明するのが難しくなります。複雑度4の関数に「これは何をするのか」と尋ねると、きれいな答えが得られます。複雑度22の関数に4段階のネストされた分岐で同じ質問をすると、正直な答えは「どのパスについて聞いているかによる」です。その曖昧さが新人エンジニアの初日を遅くし、リポジトリレベルで複雑度を追跡する価値がある理由です。
誰も指標に入れない実際のコスト:オンボーディング時間
これはダッシュボードに表示されない部分です。新人エンジニアがチームに参加する場合、「サイクロマティック複雑度23」を経験しません。彼らが経験するのは:このファイルを開いたが、どのブランチが実行されるか分からず、40分読んでいます。
いくつかのオンボーディングサイクルで粗く測定しました:複雑度スコアが15を超える関数は、新人がウォークスルーで正しく説明するのに、スコアが8未満の関数より約3~4倍時間がかかりました。高複雑度の関数がより多くのことをしたからではなく、どの分岐がどの条件で実行されるかを追跡するのに実際の順序読み時間が必要だからです。誰も最初のパスでネストされた条件を正しくスキャンしません。
これは頻繁にオンボーディングするチームにとって指標が価値を発揮する場所です。これは本当のコード品質数ではありません。それは「次にそれを書かなかった人にこれはどれだけの分数の負担をかけるか」のプロキシです。そのように追跡して下さい。そうすれば、閾値の会話はずっと具体的になります。

ゲート設定:チームをブロックしない方法
ゲート前に測定してください。以下のいずれかを選んでください:
Python:
radon cc -a -s .は各関数を成績表で表示;radon cc -n c .はC以下をフィルタ。JavaScript / TypeScript:ESLintの
complexityルール、開始時は['warn', { max: 15 }]で設定。Go:
gocyclo、パッケージを指定すると閾値を超える全関数を表示。Java / C#:SonarQueueかPMD、チームが両方とも実行していればCIに既に接続されている。
ゲートを有効にする前に、リポジトリ全体で一度実行してください。ベースラインを得られ、おそらく誰も現在のチーム内にいない40以上の範囲の関数がいくつか見つかります。それらに遡及的にビルドをブロックしません。それはlinterをルーティングすることを教えるだけです。
新規コードでは10-15でゲート。20を超える全てを自動的な却下ではなく、追加レビュアーでフラグ;そのうちいくつか、平坦なswitchなどは問題ありません。50を過ぎた全てを誰かのPRでのコメントではなく、技術債チケットとして扱ってください。

制限が有効なのは、ツールチェーンが毎回同じ方法でそれを実行するときだけです。1つの期限のために一度放棄されるルールは永遠に放棄されます。
スコアを追うか、関数を追うか
10でハードゲートし、平坦で、退屈で、読みやすい関数を出荷するチームは良好な状態です。10でハードゲートし、関数を4つの小さなものに分割し始め、誰も3つのタブを開かずにトレースできない鎖で相互に呼び出す関数を出荷するチームは、数字をより良くし、コードベースを悪くしました。
サイクロマティック複雑度は火災警報器であり、消火器ではありません。どこを見るかを教えてくれます。そこで何をするかは教えてくれません。スコアを目標として扱う代わりに、それが代理すべき読みやすさは、チームが緑のダッシュボードと、新人が貢献できるようになるまで3週間かかるリポジトリを持つ方法です。