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.
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 + 2PGdzie:
E = liczba krawędzi w grafie (połączenia między instrukcjami)
N = liczba węzłów (instrukcje, każda linia kodu to węzeł)
P = liczba spójnych komponentów (zazwyczaj 1, chyba że funkcja ma wiele głównych ścieżek)
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 totalLiczba 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 TrueLiczenie punkt po punkcie:
if not itemsto decyzja 1: jeśli brak pozycji, zwróć Falseif customer.age < 18to decyzja 2: jeśli klient niepełnoletniif payment.type != "parental_consent"to decyzja 3: jeśli brak zgody rodzicaif payment.amount < sum(items)to decyzja 4: jeśli za mało pieniędzy
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.

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:
1 do 10: Niska złożoność. Kod łatwy do testowania i utrzymania. To, do czego dążymy. Testowanie manualne OK, testy automatyczne zalecane. Funkcje na tym poziomie są czytelne i nie stanowią zagrożenia dla jakości. To ideał.
11 do 15: Umiarkowana złożoność. Warto się zastanowić nad refaktoringiem. Funkcja może pracować, ale polecany przegląd kodu. Testowanie powinno być bardziej dokładne, mogą się zacząć pojawiać pierwsze problemy z czytelnością. Wiele zespołów już tutaj włącza alarmy.
16 do 20: Wysoka złożoność. Testowanie staje się pracochłonne, błędy trudne do wyłapania. Refaktoruj teraz, zanim to przytłocze projekt. Na tym poziomie kod zaczyna być koszmarny w utrzymaniu. Większość zespołów zabrania mergowania.
21+: Krytyczna złożoność. Kod praktycznie niemożliwy do pełnego testowania i zrozumienia. Rozbij funkcję natychmiast. Nawet autorzy zapominają, co robił kod po kilku miesiącach. To jest alarm maximum.
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 FalseGoogle 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ść).

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 NonePo:
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 = 2Każ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:
Trudność w pisaniu testów: każda ścieżka wymaga osobnego testu. Przy CC = 20, potrzebujesz co najmniej 20 testów, każdy z własnym setupem danych.
Wysokie ryzyko regresji: zmiana jednego warunku może zepsuć wiele scenariuszy. Tester nie ma szansy wszystkiego pokryć.
Problemy z czytelnością: nowa osoba w zespole spędzi dni na zrozumieniu logiki. Zwiększa się czas onboardingu.
Trudności w debugowaniu: która ścieżka jest odpowiedzialna za buga? To trudne w kodzie o CC > 15.
Wypaląnie zespołu: utrzymywanie skomplikowanego kodu jest wyczerpujące.
Narzędzia do analizy CC
Nie musisz liczyć CC ręcznie. Narzędzia robią to za ciebie:
SonarQube: najpopularniejszy, raporty per-funkcja, integracja CI/CD
Pylint (Python): raport tekstowy, CC dla każdej funkcji
ESLint +
eslint-plugin-complexity(JavaScript): real-time feedbackClang (C/C++): komplexity warnings przy
clang-checkRadon (Python): CLI, prosta liczba dla każdej funkcji
Cognitive Complexity (SonarSource): nowsza alternatywa
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.