יסודות הסיבוכיות הציקלומטית

היסודות של מורכבות ציקלומטית ומדוע כל מתכנת צריך לדעת על זה

לכל פונקציה שאתה כותב יש ציון סיבוכיות. אם הפונקציה אינה מכילה נקודות החלטה, לא ifלא else, אין לולאה, אין switch, הסיבוכיות הציקלומטית שלה היא 1: בדיוק נתיב אחד דרך הקוד. הוסף if משפט נוסף והוא הופך ל-2. הוסף עוד אחד והוא הופך ל-3. כאשר פונקציה צברה תריסר ענפים מותנים על פני מספר רמות מקוננות, המורכבות הציקלומטית שלה עשויה להיות 15 או 20, ובדיקתה המלאה דורשת מספר מתאים של מקרי בדיקה, שכל אחד מהם מכסה נתיב ביצוע נפרד.

מורכבות ציקלומטית (CC) הוצגה על ידי תומאס ג'יי מקייב בשנת 1976 כמדד כמותי למורכבות הלוגית של זרימת הבקרה של תוכנית. היא נותרה אחת ממדדי איכות הקוד השימושיים ביותר מבחינה מעשית משום שהשלכותיה הן קונקרטיות וניתנות ליישום: ציון מורכבות מציין את המספר המינימלי של מקרי בדיקה הדרושים לכיסוי נתיב מלא, מנבא כמה קשה יהיה להבין ולשנות את הקוד, ומזהה את הפונקציות שסביר להניח שהן מכילות פגמים שלא התגלו. מדריך זה מכסה את הנוסחה, את הספים, דוגמאות ספציפיות לשפה, טכניקות שיפוץ שבאמת מפחיתות את המורכבות, וכיצד למדוד ולעקוב אחריה באופן אוטומטי.

SMART TS XL

עוזר לך לשלוט במורכבות ציקלומטית, לייעל את הביצועים ולמנוע באגים נסתרים

גלו עוד…

מהי מורכבות ציקלומאטית?

מורכבות ציקלומטית מודדת את מספר הנתיבים הבלתי תלויים ליניארית דרך קוד המקור של תוכנית. תומס ג'יי מקייב גזרה מתורת הגרפים: כל תוכנית יכולה להיות מיוצגת כגרף זרימת בקרה שבו צמתים הם פקודות וצלעות הן הזרימות האפשריות ביניהם. הנוסחה היא:

CC = E - N + 2P

איפה:

  • E = מספר הקצוות בגרף זרימת הבקרה
  • N = מספר צמתים
  • P = מספר הרכיבים המחוברים (בדרך כלל 1 עבור פונקציה אחת)

עבור חישוב מעשי, ישנה גרסה מקבילה פשוטה יותר: CC = מספר נקודות החלטה + 1. כל if, else if, while, for, case, catch, &&, ו || מוסיף נקודת החלטה אחת. נקודת ה-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;
}

ספים: איזה ציון מקובל?

ההנחיות המקוריות של מקייב, שנותרו המצוטטות ביותר, מגדירות ארבע רמות סיכון:

ציון CCרמת סיכוןפרשנות
1 - 10נמוךפשוט, בנוי היטב, קל לבדיקה
11 - 20לְמַתֵןמורכב יותר; נדרש מאמץ בדיקה מוגבר
21 - 50גָבוֹהַמורכב וקשה לבדיקה; מומלץ לבצע שינויים בפקטורינג
> 50גבוה מאודלא ניתן לבדיקה בפועל; סיכון איכות חמור

סף 10 הוא המגבלה הנאכפת ביותר בשערי איכות CI/CD. סף המורכבות הקוגניטיבית המוגדר כברירת מחדל של SonarQube הוא 15 (מדד קשור אך שונה). הנחיות NIST למערכות קריטיות לבטיחות ממליצות על מקסימום של 10 לכל מודול.

ניואנס חשוב אחד: CC מודד מורכבות מבנית, לא מורכבות סמנטית. פונקציה עם CC = 8 המיישמת חישוב פיננסי מורכב עשויה להיות קשה יותר להבנה מאשר פונקציה עם CC = 15 המורכבת מבדיקות הגנתיות פשוטות. השתמשו ב-CC כסימן לחקירה, לא כפסק דין סופי.

מורכבות ציקלומאטית בשפות שונות

פיתון

מבני החלטה של ​​פייתון התורמים ל-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)
    ]

כלים למדידת פייתון 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. התוספת המרכזית: משפטי ביטוי LINQ ב-C# ושרשראות טרנריות ב-TypeScript מוסיפים כל אחד נקודות החלטה.

צארפ

// 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.

קובול

       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 פירושו שפסקאות בדרך כלל ארוכות יותר מפונקציות מקבילות בשפות מודרניות. תוכניות COBOL עם CC מעל 50 לפסקה נפוצות בבסיסי קוד מדור קודם ומייצגות את המטרות בעלות העדיפות הגבוהה ביותר הן לתכנון שיפוץ והן למודרניזציה.

כיצד לחשב מורכבות ציקלומטית: שלוש שיטות

שיטה 1: ספירת נקודות החלטה + 1 השיטה הידנית המהירה ביותר. ספרו כל if, else if, while, for, case, catch, &&, || בפונקציה. הוסף 1 עבור הפונקציה עצמה.

שיטה 2: גרף זרימת בקרה צייר את הפונקציה כגרף: צומת אחד לכל משפט או בלוק, צלעות לכל זרימת בקרה. החל CC = E - N + 2.

שיטה 3: כלי אוטומטי. השיטה המעשית היחידה לכל דבר מעבר לפונקציה טריוויאלית. רוב כלי הניתוח הסטטי מחשבים CC באופן אוטומטי ומשלבים אותו בצינורות CI/CD.

טכניקות שיפוץ שבאמת מפחיתות מורכבות

סעיפי שמירה (חזרות מוקדמות)

סעיפי Guard יוצאים מהפונקציה מוקדם כאשר תנאים מוקדמים נכשלים, ובכך מבטלים ענפים של 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 לא יורד, אותן נקודות החלטה קיימות, אבל הקוד הופך להיות הרבה יותר קל לקריאה ולבדיקה. הפחתה אמיתית של CC דורשת ביטול נקודות החלטה, לא רק סידורן מחדש.

שיטות חילוץ

העברת קבוצות לוגיות של החלטות לשיטות בעלות שם מפחיתה את ה-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

אכיפת CC אוטומטית בצינורות CI/CD מונעת הצטברות בלתי נראית של מורכבות בין סקירות קוד.

יאמל

# 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:

יאמל

# 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

שער האיכות צריך לחסום על קוד חדש בעל מורכבות גבוהה, לא על כל בסיס הקוד הישן, שעשוי כבר להיות בעל CC גבוה שמטופל בנפרד.

מורכבות ציקלומטית ב-COBOL ובבסיסי קוד מדור קודם

עבור מערכות ארגוניות בהן תוכניות COBOL עשויות להכיל פסקאות עם CC העולה על 50 או אפילו 100, ניתוח מורכבות ציקלומטיקה משרת מטרה עיקרית שונה מזו שהוא משרת עבור בסיסי קוד מודרניים. השאלה אינה "האם עלינו לבצע שינויים בפונקציה זו?", אלא "מהו סיכון ההגירה של תוכנית זו, ובאיזה סדר עלינו לבצע מודרניזציה?"

תוכנית COBOL עם CC = 80 בפסקה הראשית שלה כוללת 80 נתיבי ביצוע עצמאיים, שכל אחד מהם דורש מקרה בדיקה כדי לאמת. אם לתוכנית חסרים מקרי בדיקה, דבר שרוב תוכניות COBOL מדור קודם דורשות, ציון ה-CC הוא המנבא העיקרי לכמה תרחישי אימות יש לבנות לפני שניתן יהיה לראות כל עבודת המרה בטוחה.

SMART TS XL"S ניתוח קוד סטטי מחשבת סיבוכיות ציקלומטית עבור COBOL, JCL, RPG, PL/I וכל השפות המודרניות בו זמנית, ומייצרת את התפלגות CC ברמת תיק ההשקעות שהופכת את החלטות רצף המודרניזציה למבוססות ראיות. תוכניות עם ציוני CC הגבוהים ביותר ומספר הקוראים הרב ביותר (fan-in גבוה) הן יעדי ההגירה בסיכון הגבוה ביותר, כמתואר בהקשר של קביעת מדדי אינדקס תחזוקה עבור יישומי COBOL, CC הוא מרכיב אחד של תמונה רחבה יותר של איכות הכוללת גם את נפח האלסטד ושורות קוד.

יכולת ניתוח ההשפעה משתמשת בסיווג המורכבות מבוסס CC כדי לקבוע מה צריך לאמת כאשר כל תוכנית בעלת מורכבות גבוהה משתנה: CC גבוה יותר פירושו נתיבי ביצוע רבים יותר, מה שאומר יותר תרחישי בדיקה שיש לאמת כדי לאשר שקילות התנהגותית לפני ואחרי כל שינוי.

עבור צוותים המתכננים תוכניות מודרניזציה מדור קודם , התפלגות ה-CC על פני תיק העבודות היא הקלט לרצף גלי ההגירה: תוכניות בעלות CC נמוך עם מעט קוראים עוברות מוקדם; תוכניות בעלות CC גבוה עם קוראים רבים עוברות אחרונות, לאחר שהצוות בנה מומחיות על הרכיבים הפשוטים יותר ותשתית הבדיקה קיימת כדי לאמת את המורכבים.

מורכבות אינה האויב, מורכבות בלתי נראית כן

מורכבות ציקלומטית היא אחת ממדדי איכות הקוד הבודדים שקשור ישירות ליכולת הבדיקה, מספר מקרי הבדיקה הנדרשים לכיסוי מלא של הנתיב שווה, מעצם הגדרתו, לפחות לציון ה-CC. קשר זה הופך אותה לניתנת לפעולה באופן מעשי באופן שמדדי איכות רבים אינם כאלה.

ניהול CC דורש הבנה שהמורכבות מצטברת בהדרגה. if משפט שנוסף לפונקציה הולכת וגדלה הוא סבירה באופן אינדיבידואלי. התוצאה של שלוש שנים של החלטות סבירות באופן אינדיבידואלי יכולה להיות פונקציה עם CC = 40 שאף אחד לא רוצה לגעת בה משום שהיא באמת מורכבת מדי מכדי להסיק לגביה בבטחה. הכלים והטכניקות במדריך זה, פסוקי הגנה, חילוץ מתודות, פולימורפיזם, פירוק מותנה ושערי איכות CI/CD, קיימים כדי למנוע הצטברות זו להתרחש באופן בלתי נראה ולטפל בה באופן שיטתי כאשר היא כבר קרה.