サイクロマティック複雑性の基礎

循環的複雑性の基礎と、すべてのプログラマが知っておくべき理由

作成するすべての関数には複雑度スコアがあります。関数に決定点がない場合、 ifelseループなし、なし switch、その循環的複雑度は 1 です。コードを通るパスはちょうど 1 つです。 if 条件分岐を1つ追加すると2になります。もう1つ追加すると3になります。関数が複数のネストされたレベルにわたって12個の条件分岐を蓄積すると、循環的複雑度は15または20になる可能性があり、それを完全にテストするには、それぞれ異なる実行パスをカバーする対応する数のテストケースが必要になります。

サイクロマティック複雑度(CC)は、1976年にトーマス・J・マッケイブによって、プログラムの制御フローの論理的な複雑さを定量的に測定する指標として導入されました。その意味するところが具体的で実行可能なため、CCは最も実用的なコード品質指標の一つとして今もなお広く用いられています。複雑度スコアは、パス全体を網羅するために必要なテストケースの最小数を示し、コードの理解や修正の難易度を予測し、未発見の欠陥が含まれている可能性が最も高い関数を特定します。このガイドでは、計算式、しきい値、言語固有の例、実際に複雑度を低減するリファクタリング手法、そしてCCを自動的に測定・追跡する方法について解説します。

SMART TS XL

循環的複雑性をマスターし、パフォーマンスを最適化し、隠れたバグを防ぐのに役立ちます

さらに詳しく…

サイクロマティック複雑度とは何ですか?

循環的複雑度は、プログラムのソースコード内の線形独立なパスの数を測定する指標です。トーマス・J・マッケイブはグラフ理論からこの指標を導き出しました。すべてのプログラムは制御フローグラフとして表現でき、ノードはステートメント、エッジはそれらの間の可能なフローを表します。その式は次のとおりです。

CC = E - N + 2P

どこ:

  • E =制御フローグラフのエッジ数
  • N = ノード数
  • P =接続されたコンポーネントの数(通常、単一の機能の場合は1)

実用的な計算においては、より単純な等価式が存在する。 CC = 決定点の数 + 1。 すべての if, else if, while, for, case, catch, &&, || 決定点を1つ追加します。どの関数の開始CCも1です。

ジャワ

// CC = 1: no decision points
public String greet(String name) {
    return "Hello, " + name;
}

// CC = 3: two decision points (two if statements)
public String classify(int score) {
    if (score >= 90) return "Excellent";
    if (score >= 70) return "Satisfactory";
    return "Needs improvement";
}

// CC = 5: four decision points (three conditions + one loop)
public double calculateTotal(List<Item> items, boolean isMember, boolean isHoliday) {
    double total = 0;
    for (Item item : items) {          // +1
        total += item.getPrice();
    }
    if (isMember) total *= 0.9;        // +1
    if (isHoliday) total *= 0.95;      // +1
    if (total > 100) total -= 5;       // +1
    return total;
}

基準値:どのくらいの点数が許容範囲か?

マッケイブ氏の当初の指針(現在でも最も広く引用されている)では、4つのリスクレベルが定義されている。

CCスコアリスクレベル解釈
1 – 10ローシンプルで構造がしっかりしており、テストも容易です。
11 – 20穏健派より複雑になり、テスト作業の増加が必要となる。
21 – 50ハイ複雑でテストが困難。リファクタリングを推奨。
> 50すごく高い実際にはテスト不可能。深刻な品質リスク

CI/CDの品質ゲートにおいて、 10という閾値は最も一般的に適用される制限値です。SonarQubeのデフォルトの認知複雑性閾値は15です(関連性はあるものの、異なる指標です)。NISTの安全性が重要なシステムに関するガイドラインでは、モジュールあたり最大10を推奨しています。

重要な注意点として、 CCは構造的な複雑さを測定するものであり、意味的な複雑さを測定するものではありません。複雑な財務計算を実行するCC=8の関数は、単純な防御チェックで構成されるCC=15の関数よりも理解しにくい場合があります。CCは調査のきっかけとして活用し、最終的な判断基準として使用しないでください。

異なる言語における循環的複雑性

Python

PythonのCCに貢献する決定構造: if, elif, else (カウント対象外、条件なし) for, while, try/except (それぞれ except カウント)、 with (カウントしない)、およびブール演算子 and/or 条件で。

パイソン

# CC = 1
def format_name(first: str, last: str) -> str:
    return f"{first} {last}"

# CC = 4: three decision points
def calculate_discount(price: float, is_member: bool, is_holiday: bool) -> float:
    discount = 0.0
    if is_member:        # +1
        discount += 0.10
    if is_holiday:       # +1
        discount += 0.05
    if price > 100:      # +1
        discount += 0.02
    return price * (1 - discount)

# CC = 6: five decision points (list comprehension counts as a loop)
def process_orders(orders: list[dict]) -> list[dict]:
    return [
        {**order, "total": order["qty"] * order["price"]}  # +1 (comprehension)
        for order in orders
        if order["qty"] > 0                                 # +1 (filter condition)
        if order["price"] > 0                              # +1 (second filter)
    ]

PythonのCC測定ツール: radon (radon cc src/ -s), flake8-cognitive-complexity, pylint 複雑性プラグインを使用したSonarQubeのPython分析。

Java

ジャワ

// CC = 7: complex authentication with multiple conditions
public AuthResult authenticate(String userId, String password, boolean isMfa) {
    if (userId == null || password == null) return AuthResult.INVALID;  // +2 (||)
    User user = userRepository.findById(userId);
    if (user == null) return AuthResult.NOT_FOUND;                      // +1
    if (!user.checkPassword(password)) return AuthResult.WRONG_PASSWORD;// +1
    if (isMfa && !user.hasMfaEnabled()) return AuthResult.MFA_REQUIRED; // +2 (&&)
    return AuthResult.SUCCESS;
}
// CC = 1 + 2 + 1 + 1 + 2 = 7

それを減らす:

ジャワ

// After refactoring: CC = 3 (main method) + small helpers with CC = 2 each
public AuthResult authenticate(String userId, String password, boolean isMfa) {
    if (hasInvalidInputs(userId, password)) return AuthResult.INVALID;
    User user = findVerifiedUser(userId, password);
    if (user == null) return AuthResult.WRONG_PASSWORD;
    if (requiresMfa(user, isMfa)) return AuthResult.MFA_REQUIRED;
    return AuthResult.SUCCESS;
}

private boolean hasInvalidInputs(String userId, String password) {
    return userId == null || password == null;  // CC = 2
}

private boolean requiresMfa(User user, boolean isMfa) {
    return isMfa && !user.hasMfaEnabled();      // CC = 2
}

Java CC 用ツール: Checkstyle、PMD、SonarQube、IntelliJ IDEA 内蔵、 SMART TS XL.

C#とTypeScript

C#とTypeScriptはJavaと同じルールに従います。重要な追加要素は、C#のLINQ式句とTypeScriptの三項演算子チェーンがそれぞれ分岐点を追加する点です。

Cシャープ

// CC = 5: switch with four cases
public decimal GetShippingCost(string zone) => zone switch {
    "domestic"      => 5.99m,    // +1
    "eu"            => 15.99m,   // +1
    "international" => 29.99m,   // +1
    "express"       => 49.99m,   // +1
    _               => throw new ArgumentException($"Unknown zone: {zone}")
};

COBOL

COBOLのCCに貢献する決定構造: IF/ELSE, EVALUATE WHEN (各WHEN句) PERFORM UNTIL, PERFORM VARYING ... WITH TEST BEFORE/AFTER, AT END, ON EXCEPTION, NOT ON EXCEPTION, ON SIZE ERROR.

COBOL

       CALCULATE-DISCOUNT.
           IF WS-CUSTOMER-TYPE = 'GOLD'               *> +1
               IF WS-PURCHASE-AMT > 1000              *> +1
                   COMPUTE WS-DISCOUNT = 0.20
               ELSE                                    *> (no increment)
                   COMPUTE WS-DISCOUNT = 0.15
           ELSE IF WS-CUSTOMER-TYPE = 'SILVER'        *> +1
               COMPUTE WS-DISCOUNT = 0.10
           ELSE                                        *> (no increment)
               COMPUTE WS-DISCOUNT = 0.05
           END-IF
           EVALUATE TRUE
               WHEN WS-REGION = 'NORTH' PERFORM APPLY-REGIONAL-RATE  *> +1
               WHEN WS-REGION = 'SOUTH' PERFORM APPLY-SOUTHERN-RATE  *> +1
           END-EVALUATE.
           *> Total CC = 1 + 5 = 6

COBOLの冗長な構文は、段落が現代の言語における同等の関数よりも長くなることを意味します。段落あたり50文字を超えるCOBOLプログラムは、レガシーコードベースでよく見られ、リファクタリングと近代化計画の両方において最優先の対象となります。

サイクロマティック複雑度の計算方法:3つの手法

方法1:決定点を数える + 1 最も速い手動の方法。 if, else if, while, for, case, catch, &&, || 関数内で、関数自体に1を加算します。

方法2:制御フロー図 関数をグラフとして描画します。ステートメントまたはブロックごとにノードを1つ、制御フローごとにエッジを作成します。 CC = E - N + 2.

方法3:自動化ツール 単純な関数を超えるものに対しては、これが唯一実用的な方法です。ほとんどの静的解析ツールは、CCを自動的に計算し、CI/CDパイプラインに統合します。

実際に複雑さを軽減するリファクタリング手法

ガード条項(早期返品)

ガード句は、前提条件が満たされない場合に早期に関数を終了するため、else分岐がなくなり、ネストの深さが軽減されます。

パイソン

# Before: deeply nested, CC = 5
def process_order(order):
    if order is not None:
        if order.is_valid():
            if order.has_stock():
                if order.payment_cleared():
                    return fulfill_order(order)
                else:
                    return "Payment failed"
            else:
                return "Out of stock"
        else:
            return "Invalid order"
    else:
        return "No order"

# After: flat, CC = 5 (same complexity, dramatically better readability)
def process_order(order):
    if order is None: return "No order"
    if not order.is_valid(): return "Invalid order"
    if not order.has_stock(): return "Out of stock"
    if not order.payment_cleared(): return "Payment failed"
    return fulfill_order(order)

複雑度(CC)は減少しません。同じ決定ポイントが存在しますが、コードの可読性とテスト性は格段に向上します。真の複雑度削減には、決定ポイントの配置換えだけでなく、それらを完全に排除することが必要です。

抽出方法

論理的な判断グループを名前付きメソッドに移動することで、呼び出し関数の複雑性を低減しつつ、より小さくテスト可能な単位に複雑性を分散させることができます。

ジャワ

// Before: one method doing everything, CC = 9
public double calculateInvoiceTotal(Invoice invoice, Customer customer) {
    double subtotal = 0;
    for (LineItem item : invoice.getItems()) {
        subtotal += item.getQuantity() * item.getUnitPrice();
        if (item.isTaxable()) subtotal += item.getPrice() * 0.1;
    }
    if (customer.isMember()) subtotal *= 0.9;
    if (customer.hasVoucher()) subtotal -= customer.getVoucherValue();
    if (subtotal < 0) subtotal = 0;
    return subtotal;
}

// After: main method CC = 4, helpers have CC = 2-3 each
public double calculateInvoiceTotal(Invoice invoice, Customer customer) {
    double subtotal = computeLineItemTotal(invoice.getItems());
    subtotal = applyCustomerDiscounts(subtotal, customer);
    return Math.max(0, subtotal);
}

条件分岐をポリモーフィズムに置き換える

関数が型や状態に基づいて分岐する場合、ポリモーフィズムは分岐を完全に排除します。

ジャワ

// Before: switch on payment type, CC grows with each new type
public void processPayment(String type, double amount) {
    switch (type) {
        case "CREDIT":  processCreditCard(amount); break;
        case "PAYPAL":  processPayPal(amount);     break;
        case "CRYPTO":  processCrypto(amount);     break;
        default: throw new IllegalArgumentException("Unknown type: " + type);
    }
}

// After: new payment types require no changes to this method, CC = 1
public interface PaymentProcessor {
    void process(double amount);
}

public void processPayment(PaymentProcessor processor, double amount) {
    processor.process(amount);  // no branching
}

複雑な条件文の分解

複雑なブール式を、その意図を明確に示す名前付きメソッドに抽出する。

パイソン

# Before: dense boolean logic, hard to understand, easy to mis-test
if user.age >= 18 and user.country in ALLOWED_COUNTRIES and not user.is_banned and user.verified:
    grant_access()

# After: named predicate, self-documenting, unit-testable independently
def is_eligible_for_access(user: User) -> bool:
    return (
        user.age >= 18
        and user.country in ALLOWED_COUNTRIES
        and not user.is_banned
        and user.verified
    )

if is_eligible_for_access(user):
    grant_access()

CI/CDパイプラインにおける循環的複雑性

CI/CDパイプラインにおける自動化されたCC(複雑性チェック)の実施は、コードレビューの合間に複雑性が目に見えない形で蓄積されるのを防ぎます。

ヤムル

# GitHub Actions: fail PR if any function exceeds CC threshold
name: Code Quality

on: [pull_request]

jobs:
  complexity-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install radon (Python CC tool)
        run: pip install radon

      - name: Check cyclomatic complexity
        run: |
          radon cc src/ --min C --show-complexity
          # Fails if any function has CC grade C (11-15) or worse
          radon cc src/ --min C --total-average | grep -q "Average complexity" \
            && echo "Complexity check passed" \
            || (echo "Functions with high complexity found" && exit 1)

SonarQubeを使用したJavaの場合:

ヤムル

# SonarQube quality gate blocks merge if CC exceeds threshold
- name: SonarCloud Scan
  uses: SonarSource/sonarcloud-github-action@master
  env:
    SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
  with:
    args: |
      -Dsonar.qualitygate.wait=true
      -Dsonar.java.complexity.Function.threshold=10

品質ゲートは、複雑度の高い新規コードのみをブロックするべきであり、既に複雑度が高く、別途対処されている可能性のある既存コードベース全体をブロックするべきではない。

COBOLおよびレガシーコードベースにおける循環的複雑度

COBOLプログラムにおいて、循環的複雑度(CC)が50、あるいは100を超える段落が含まれる可能性があるエンタープライズシステムでは、循環的複雑度分析は、現代のコードベースとは異なる主要な目的を果たします。問題は「この関数をリファクタリングすべきか?」ではなく、「このプログラムの移行リスクはどの程度か、そしてどのような順序で近代化すべきか?」です。

メイン段落全体でCC値が80のCOBOLプログラムには、80個の独立した実行パスがあり、それぞれを検証するためのテストケースが必要です。ほとんどのレガシーCOBOLプログラムがそうであるように、プログラムにテストケースがない場合、CC値は、変換作業が安全であるとみなされる前に構築する必要のある検証シナリオの数を予測する主要な指標となります。

SMART TS XLさん 静的コード分析 COBOL、JCL、RPG、PL/I、およびすべての最新言語の循環的複雑度を同時に計算し、ポートフォリオレベルのCC分布を生成することで、近代化の順序付けの決定をエビデンスベースにします。CCスコアが最も高く、呼び出し元が最も多い(ファンインが高い)プログラムは、移行リスクが最も高い対象となります。 COBOLアプリケーションの保守性指標を確立するCCは、ハルステッドボリュームやコード行数などを含む、より広範な品質評価の構成要素の一つです。

影響分析機能は、CCベースの複雑性分類を使用して、複雑性の高いプログラムに変更があった場合に検証する必要のある範囲を定めます。CCが高いほど実行パスが多くなり、変更の前後で動作の同等性を確認するために検証する必要のあるテストシナリオが多くなります。

レガシーシステムの近代化プログラムを計画しているチームにとって、ポートフォリオ全体におけるコンポーネント構成(CC)の分布は、移行ウェーブの順序付けのインプットとなります。呼び出し元が少ない低CCプログラムは早期に移行し、呼び出し元が多い高CCプログラムは、チームがよりシンプルなコンポーネントに関する専門知識を蓄積し、複雑なコンポーネントを検証するためのテストインフラストラクチャが整った後に、最後に移行します。

複雑さは敵ではない、目に見えない複雑さが敵だ

循環的複雑度は、テスト容易性と直接結びつく数少ないコード品質指標の一つです。パス全体を網羅するために必要なテストケースの数は、定義上、循環的複雑度スコア以上になります。この結びつきにより、多くの品質指標とは異なり、循環的複雑度は実用的なツールとなります。

CCを管理する規律には、複雑性が段階的に蓄積されるという理解が必要です。 if 関数にステートメントを追加することは、個々には妥当な判断です。しかし、3年間、個々に妥当な判断を積み重ねた結果、CC = 40 の関数ができあがり、誰も触りたがらなくなる可能性があります。なぜなら、それは本当に複雑すぎて安全に推論できないからです。このガイドで紹介するツールとテクニック、ガード句、メソッド抽出、ポリモーフィズム、条件分解、CI/CD 品質ゲートは、そのような蓄積が目に見えない形で起こるのを防ぎ、既に発生してしまった場合には体系的に対処するために存在します。