循環的複雑度:コードの複雑さを数字で測る方法
要約
複雑なコードのリスクを数値化する循環的複雑度の基礎知識と実践的な改善手法を紹介。Python の実例を通じて、計算方法と5つの低減テクニックを学ぶ。
複雑なコードは読みづらく、バグも増えやすい。でも「複雑さ」ってどうやって測るんでしょう?そこで登場するのが循環的複雑度(Cyclomatic Complexity)。数学的な方法で、コードの複雑さを1つの数字で表せます。
この指標を理解すれば、コードの品質をより客観的に判断できるようになります。テストすべき箇所も見つけやすくなるし、リファクタリングの優先順位もつけやすい。さらに、Excel の複雑な数式にも同じ考え方が使えます。
循環的複雑度の計算式:M = E - N + 2P
循環的複雑度を求める最も一般的な公式がこれです。
M = E - N + 2P
各記号の意味:
M = 循環的複雑度(Cyclomatic Complexity)
E = 制御フローグラフの辺の数
N = ノードの数
P = 独立した接続成分の数(通常は 1)
実際には、決定点の数を数えるショートカット法がより直感的です。
CC = 判定(条件)の数 + 1
こちらの方が覚えやすいし、手計算でもサッと出せます。if、switch、for、while、catch などの判定ポイントを1つ数えるたびに1を足していくだけです。
実例:Python の関数で計算してみる
具体例がないとわかりにくいですよね。こんな Python の関数を見てください。
def process_order(order):
if order.is_valid():
if order.total() > 1000:
discount = 0.2
else:
discount = 0.1
else:
return None
if order.user_type() == "vip":
discount = 0.3
elif order.user_type() == "regular":
discount = 0.15
else:
discount = 0
return order.total() * (1 - discount)
この関数の循環的複雑度は?判定を数えます:
if order.is_valid()→ 1if order.total() > 1000→ 1if order.user_type() == "vip"→ 1elif order.user_type() == "regular"→ 1else→ 1(追加される)
ショートカット法:CC = 判定の数 + 1 = 4 + 1 = 5
この関数は複雑度 5。そこまで悪くない範囲ですが、機能が増えるにつれてこの数字は膨れていきます。
複雑度スコアの意味:何が「危険」なのか
数字が出たら、それが何を意味するのか知る必要があります。業界の一般的な基準はこちら。
1~10:低リスク。テストしやすく、保守しやすい。理想的。
11~15:中程度のリスク。テストカバレッジを上げる必要あり。
16~20:高リスク。リファクタリング推奨。バグが潜みやすい。
21 以上:危険度最高。即座にリファクタリングすべき。テストも困難。
チーム全体の平均を取ることも大事です。「この関数だけ複雑」ではなく、「全体的に複雑さが高い」というなら、コードレビューのプロセスや設計の見直しが必要かもしれません。
循環的複雑度と認知的複雑度:よく混同される違い
ここで注意が必要です。「複雑度」という言葉は複数の定義があります。
循環的複雑度(Cyclomatic Complexity) = テストの難しさを測る指標。判定の数で純粋に計算します。
認知的複雑度(Cognitive Complexity) = 人間がコードを読んで理解するのがどれだけ難しいか。ネストの深さや論理の複雑さをより詳しく数値化します。

例えば、ネストが深い if 文は、循環的複雑度は変わらなくても、認知的複雑度は上がります。つまり、循環的複雑度は「テストすべき経路の数」で、認知的複雑度は「読むのにかかる脳力」なんです。
Linter(SonarQube など)を設定するとき、どちらを優先するかで戦略が変わります。多くの現代的なツールは認知的複雑度を推奨しています。
Excel の複雑な数式も同じ問題を抱えている
プログラマーだけの話じゃありません。Excel や Google Sheets で複雑な数式を組んだことがあるなら、この問題は他人事ではないはずです。
=IF(A1>100,IF(B1="approved",A1*0.9,A1*0.8),IF(C1>50,A1*0.7,A1))こんな入れ子数式、見たことありますよね?このネストの深さと判定の数が、「複雑さ」を生みます。
同じロジックを以下のように分割すれば、ずっと読みやすく、バグも減ります。
E2: =A1 * lookup_discount(B1, C1) (複雑度 1)ヘルパー関数に処理を分ける。これが「複雑度を下げる」ことの本質です。
循環的複雑度を下げる 5 つの方法
数字が高くなってしまった。じゃあどうするか?
1. ガード句を使う(Early Return)
ネストを減らす最も簡単な方法。条件を先に判定して、該当しなきゃ早めに return。
# Before (複雑度 4)
def validate_user(user):
if user.is_active():
if user.age >= 18:
if user.has_permission():
return True
return False
# After (複雑度 3)
def validate_user(user):
if not user.is_active():
return False
if user.age < 18:
return False
if not user.has_permission():
return False
return True2. 関数を分割する
責任を1つに絞った小さな関数に分ける。各関数の複雑度が下がれば、全体の保守性も上がります。
3. ルックアップテーブルを使う
判定が多い場合、辞書や配列で値をマッピング。
DISCOUNT_MAP = {
"vip": 0.3,
"regular": 0.15,
"guest": 0.0
}
discount = DISCOUNT_MAP.get(user_type, 0.0) # 複雑度 14. ポリモーフィズムを活用する(OOP)
クラスやインターフェースで判定を分散。if-else チェーンではなく、オブジェクト指向の仕組みで処理を振り分けます。
5. 関心の分離(Separation of Concerns)
「何をするのか」ではなく「何を判定するのか」を明確にしたら、判定ロジックと実行ロジックを別ける。テスト範囲も狭くなります。
複雑度を自動で計算するツール
手計算も大事ですが、プロジェクト全体を把握するなら自動ツールが必須。
SonarQube:最も有名。リーダーボードにも認知的複雑度を表示。CI/CD パイプラインに組み込める。
Code Climate:クラウドベース。git とも連携。複雑度に基づいて A~F ランク。
Pylint / Flake8(Python):
pylint --disable=all --enable=R0912で複雑度チェック。ESLint(JavaScript):プラグイン
eslint-plugin-sonarjsで cyclomatic complexity 測定。Roslyn Analyzers(C#):Visual Studio に統合。
ほぼ全ての言語に対応ツールがあります。チームで統一ツールを選んで、基準を決めておくと、PR レビューもやりやすくなります。
複雑度を測るなら、こんなツールが役に立つ
複雑度の計測を習慣化するには、以下のようなツールがあると便利です。
よくある質問(FAQ)
循環的複雑度が高いと何が問題なの?
判定ポイントが多いほど、テストケースが指数関数的に増えます。全ての分岐をテストしようとすると、コストが膨大になります。また、複雑なコードは読みづらく、バグも増えやすい。
複雑度 10 を超えたら即座にリファクタリング必須ですか?
必須ではありません。チームの方針次第。ただし、15 を超えたら真剣に考えるべき。新規開発では複雑度 10 以下が目安。既存コードのリファクタリングは段階的に進めるのがいい。
自動生成されたコードの複雑度はどう扱う?
計測の対象から外すか、別の基準を設けるのが一般的。自動生成コードはテスト対象にしないことも多いですし。
Google Sheets や Excel のマクロにも使える?
もちろん。VBA の関数の複雑度も同じ方法で計算できます。ただし VBA の場合、認知的複雑度の方が重要かもしれません(ネストが深くなりやすいため)。
認知的複雑度と循環的複雑度、どっちを優先すべき?
新規プロジェクトなら認知的複雑度。既存コードの改善なら循環的複雑度から始める方が実装しやすい。最終的には両方監視するのが理想的。
複雑度の基準を下げるべき小さなチームは?
むしろ下げるべき。少人数だからこそ、1 人が去ると知識が失われやすい。複雑度を低く保つことで、共有と保守がラクになります。