Złożoność cyklomatyczna: pomiar i optymalizacja kodu

Summary

Złożoność cyklomatyczna (CC) to liczba ścieżek przez kod. Wzór M = E - N + 2P, czyli liczba punktów decyzji + 1. Wynik 1-10 to norma, 11-15 ostrzeżenie, 16+ alarm. Naucz się liczyć ją ręcznie, rozróżniać od złożoności poznawczej i zmniejszać praktycznie.

Deweloper przy biurku ze skomplikowanym diagramem przepływu sterowania na ekranach

Złożoność cyklomatyczna: pomiar i optymalizacja kodu

Złożoność cyklomatyczna to liczba, która mówi ci jedno: ile decyzji (rozgałęzień) ma twój kod. Im więcej if, else i case, tym wyższy wynik, tym trudniej ten kod testować, utrzymywać i debugować.

Wzór to M = E - N + 2P. Brzmmi bardzo ładnie matematycznie, ale pokażemy to na żywych przykładach, a potem na narzędziach, które liczą to za ciebie.

Czym jest złożoność cyklomatyczna i skąd się bierze

Złożoność cyklomatyczna (CC) mierzy liczbę niezależnych ścieżek przez funkcję. Stworzył ją Thomas McCabe w 1976 roku jako metrykę testości kodu. Metryka ta stała się standardem w inżynierii oprogramowania i jest używana przez zespoły na całym świecie, od startupów po gigantów technologicznych.

Im wyższy wynik CC, tym więcej testów trzeba napisać, aby pokryć wszystkie gałęzie. Pojęcie jest proste: funkcja bez żadnych warunków (CC = 1) wymaga jednego testu. Funkcja z jednym if (CC = 2) wymaga co najmniej dwóch testów. Funkcja z trzema zagnieżdżonymi if (CC = 4) wymaga już czterech testów.

W praktyce zespoły programistyczne używają CC do monitorowania złożoności kodu. Wysoka CC oznacza ryzyko: trudniej testować, trudniej zrozumieć, większe prawdopodobieństwo bugów. To metryka, która bezpośrednio wpływa na jakość produkowanego oprogramowania. Gdy CC rośnie, spada testowość, a ryzyko regresji rośnie exponencjalnie. Dlatego wiele zespołów monitoruje tę metrykę w ciągłej integracji (CI/CD).

Wzór za złożością cyklomatyczną: M = E - N + 2P

Mathematycznie, złożoność cyklomatyczna to liczba niezależnych ścieżek przez graf przepływu sterowania. Grafem sterowania (control flow graph) jest reprezentacja wszystkich możliwych ścieżek, jakie kod może przejść w trakcie wykonania.

Wzór wygląda tak:

M = E - N + 2P

Gdzie:

Albo szybszy sposób praktyczny: policz każdy punkt decyzji (każdy if, while, for, case, catch, operator trójargumentowy ?:) i dodaj 1. Punkty decyzji to miejsca, gdzie kod może pójść w różne kierunki. To najszybsza metoda dla praktyków.

Praktyczny przykład z e-commerce:

def process_order(order):
    if order.total > 100:           # 1 decyzja
        if order.vat_paid:          # 2 decyzje
            total = order.total
        else:
            total = order.total * 1.23
    else:
        if order.shipping_needed:  # 3 decyzje
            total = order.total + 10
        else:
            total = order.total
    return total

Liczba decyzji: 3 (trzy if), czyli CC = 3 + 1 = 4. Ta funkcja ma 4 możliwe ścieżki wykonania, każda reprezentująca inny scenariusz biznesowy.

Ręcznie: konkretny przykład z drzewem decyzji

Weźmy funkcję Pythona, która waliduje zamówienie e-commerce. Funkcja musi sprawdzić kilka warunków: czy zamówienie nie jest puste, czy klient jest pełnoletni, czy klient ma zgodę rodzica jeśli jest niepełnoletni, czy ma wystarczająco pieniędzy na opłacenie.

def validate_order(items, customer, payment):
    if not items:                    # decyzja 1
        return False
    
    if customer.age < 18:            # decyzja 2
        if payment.type != "parental_consent":
            return False             # decyzja 3
    
    if payment.amount < sum(items):  # decyzja 4
        return False
    
    return True

Liczenie punkt po punkcie:

Złożoność = 4 decyzje + 1 = CC = 5.

Na grafie przepływu sterowania byłoby to 5 niezależnych ścieżek, które kod może przejść. Każda ścieżka reprezentuje inny scenariusz biznesowy: puste zamówienie, pełnoletni, niepełnoletni bez zgody, niepełnoletni ze zgodą, brak pieniędzy.

Diagram przepływu sterowania z węzłami decyzji

Co oznacza Twój wynik: normy i progi alarmowe

Thomas McCabe zaproponował te progi referencyjne, którymi posługuje się większość zespołów programistycznych na świecie:

W praktyce zespoły ustawiają własne limity. SonarQube domyślnie ostrzega przy CC > 10, zakazuje mergowania PR przy CC > 15. Facebook używa bardziej liberalnych limitów (do 20), ale małe startupy mogą chcieć ścieńszych granic (max 8). Netflix ma zaostrzony standard: maksimum CC = 7.

Złożoność cyklomatyczna vs. złożoność poznawcza

To dwa różne zwierzęta, ale łatwo je pomylić. Obie mierzą złożoność, ale innymi metrami.

Złożoność cyklomatyczna (CC) liczy liczbę decyzji, czyli ile ścieżek przez kod. To miara obiektywna, możliwa do obliczenia automatycznie. Każdy narzędzie da ten sam wynik.

Złożoność poznawcza (CC~) mierzy trudność mentalną, czyli ile pamięci musi umysł alokować, aby zrozumieć kod. Zagnieżdżone if to bardziej złożone poznawczo niż płaskie sekwencje, nawet jeśli CC jest taka sama. Mierzy to, jak długo będziesz myślał nad kodem.

Przykład ilustrujący różnicę:

# Niska CC, wysoka złożoność poznawcza:
def complex_logic(a, b, c, d, e):
    if a and b and c and d and e:  # 5 warunków, ale CC = 2
        return True
    return False

# Wysoka CC, niższa złożoność poznawcza:
if a:
    if b:
        if c:
            if d:
                if e:
                    return True  # CC = 5, ale każda decyzja wyraźna
return False

Google i SonarSource teraz preferują złożoność poznawczą do decyzji refaktoringowych, bo lepiej opisuje rzeczywisty wysiłek umysłu. CC to bardziej "testability" (testowość), CC~ to bardziej "readability" (czytelność).

Dwie osoby robiące code review przy biurku

Złożoność cyklomatyczna w Excelu i Google Sheets

Możliwe, że nigdy nie słyszałeś o CC dla formuł, ale jest tam wszędzie i sprawia tyle samo problemów co w kodzie.

Formuła Excela z zagnieżdżonymi IF:

=IF(B2>100, IF(C2="VIP", B2*0.8, B2*0.9), IF(B2>50, B2*0.95, B2))

To cztery decyzje: B2>100, czy VIP, oraz B2>50. CC = 4 + 1 = 5.

Formuła jest czytelna dzisiaj? Tak. Ale za rok, gdy będziesz jej szukać w dokumentacji, gdy ktoś inny będzie musiał ją edytować, gdy zmienią się progi biznesowe, będzie chaos. Wiele błędów w tablicach pochodzi stąd, że osoba pisała formułę z CC = 7, a nikt nie był w stanie jej przetestować. Formuła robi coś dziwnego, ale co? Nikt nie wie. I siedzi tam przez 3 lata, aż w końcu ktoś ją usuwa i przepisuje od nowa. To jeden z powodów, dla których duże korporacje wprowadzają audyty formuł i przeglądy jakości.

Pięć sposobów na zmniejszenie złożoności cyklomatycznej

1. Klauzule ochrony (guard clauses)

Zamiast zagnieżdżać if, zwróć wcześnie błędy. Płaska struktura jest łatwiej czytelna.

Przed:

def process(user, order, payment):
    if user:
        if order:
            if payment:
                return calculate(order)
    return None

Po:

def process(user, order, payment):
    if not user: return None
    if not order: return None
    if not payment: return None
    return calculate(order)

CC spada z 3 zagnieżdżeń na 3 płaskie decyzje. Kod czyta się jak lista warunków do spełnienia, nie jak piramida zagnieżdżeń.

2. Wyciągaj funkcje

Jeśli masz wielkie if z boolowskim wyrażeniem, wyciągnij logikę do osobnej funkcji:

Przed:

if validate_vat(customer) and check_shipping(order) and is_solvent(payment):
    process(order)

Po:

if order_is_valid(customer, order, payment):
    process(order)

def order_is_valid(customer, order, payment):
    return (validate_vat(customer) and check_shipping(order)
            and is_solvent(payment))

CC głównej funkcji spada (jeden warunek). Nowa funkcja jest testowalna niezależnie i ma swoją nazwę opisującą logikę.

3. Tablica przeglądowa zamiast switch

switch lub seria elif zwiększa CC liniowo. Tablica zmniejsza ją do 1.

Przed:

if status == "pending": send_email_pending()
elif status == "approved": send_email_approved()
elif status == "rejected": send_email_rejected()
elif status == "archived": log_archive()

CC = 4 + 1 = 5.

Po:

actions = {
    "pending": send_email_pending,
    "approved": send_email_approved,
    "rejected": send_email_rejected,
    "archived": log_archive
}
actions.get(status, no_op)()

CC = 1. Dodajesz nowy status? Dodajesz entry, nie modyfikujesz logiki.

4. Podziel funkcję po liniach biznesowych

Jeśli funkcja robi 10 rzeczy pod różnymi warunkami, rozbij ją:

# Zamiast jednej mega-funkcji z CC = 15:
validate_payment_and_ship_and_notify(order)

# Robi się:
validate_payment(order)   # CC = 3
ship_order(order)         # CC = 2
notify_customer(order)    # CC = 2

Każda funkcja jest niezależna, testowalna i czytalna. Łatwiej się zmienia, łatwiej się debuguje.

5. Przenieś warunki do danych

Zamist pytać kod, zapytaj dane. Reguły powinny żyć w konfiguracji:

# Zamiast:
if user.role == "admin": perms = ["read", "write", "delete"]
elif user.role == "moderator": perms = ["read", "write"]
elif user.role == "user": perms = ["read"]

# Robi się:
role_perms = {
    "admin": ["read", "write", "delete"],
    "moderator": ["read", "write"],
    "user": ["read"]
}
perms = role_perms.get(user.role, [])

Logika kodu = 0 decyzji. Reguły żyją w konfiguracji. Dodajesz nową rolę? Dodajesz do słownika, nie edytujesz kod logiki.

Błędy powszechne związane z wysoka CC

Wysoka złożoność cyklomatyczna to praktyczny problem, który wpływa bezpośrednio na prędkość pracy zespołu, jakość kodu i satysfakcję z pracy. Poniżej znajduje się lista problemów powszechnych:

Narzędzia do analizy CC

Nie musisz liczyć CC ręcznie. Narzędzia robią to za ciebie:

Uruchom jedno z tych narzędzi na swoim kodzie. Szansa na znalezienie czegoś z CC > 20? Bardzo wysoka. Wtedy widzisz dokładnie, gdzie refaktorować i co zmienić w kodzie.

Polecane narzędzia do analizy

Aby śledzić złożoność cyklomatyczną w produkcji, warto mieć narzędzia w toolsetcie:

Podsumowanie

Złożoność cyklomatyczna to liczba, która mówi ci dokładnie, czy kod będzie można testować i utrzymywać bez dodatkowych kłopotów. Wzór (M = E - N + 2P) to liczba decyzji + 1.

Wynik 1-10: norma. 11-15: uwaga. 16-20: zły. 21+: alarmowo.

Warto wiedzieć, czego szukać: zagnieżdżone if, długie switch, warunki z wieloma and i or. Te rzeczy robią kod trudnym do czytania i testowania.

Jeśli pracujesz w Excel lub Google Sheets, to samo dotyczy formuł. Formułę z CC = 7 przeczyta każdy. Formułę z CC = 12 nie przeczyta nikt za rok.

Frequently asked questions

Czym różni się złożoność cyklomatyczna od złożoności poznawczej?
Złożoność cyklomatyczna liczy punkty decyzji. Złożoność poznawcza mierzy trudność mentalną. Zagnieżdżone if ma niską CC, wysoką poznawczą.
Jaki jest bezpieczny próg złożoności cyklomatycznej?
1-10 to norma, 11-15 to uwaga, 16-20 to już wysoko, 21+ to alarm. Większość zespołów ustawia limit na 10 lub 15.
Jak policzyć CC ręcznie?
Policz każdy punkt decyzji (if, while, for, case, catch) i dodaj 1. Lub użyj wzoru M = E - N + 2P.
Czy złożoność cyklomatyczna ma znaczenie w Excelu?
Tak. Formuła z wieloma IF ma wysoką CC. Formuła z CC > 10 jest trudna do testowania i utrzymywania.
Jakie narzędzie powinien użyć do mierzenia CC?
SonarQube (uniwersalne), Pylint (Python), ESLint + plugin (JavaScript), Radon (Python CLI). Wybierz wspierające Twój stos.
Czy refaktorowanie kodu tylko aby obniżyć CC to zawsze dobra idea?
Nie zawsze. Niska CC ma sens w produkcji. W prototypach wyższa CC może być OK. Ale jeśli CC > 15, to kod zyska na refaktoringu.
Czy CC zmienia się w zależności od języka programowania?
Formuła jest ta sama (E - N + 2P), ale języki liczą węzły inaczej. Wynik jest porównywalny, może być niewielka różnica między narzędziami.