Złożoność cyklomatyczna: co budować i refaktorować

Summary

Złożoność cyklomatyczna to metryka z 1976 roku, która zlicza niezależne ścieżki wykonania w funkcji. Wyniki powyżej 10 wymagają testów, powyżej 20 refaktoryzacji przed nowymi featurami, powyżej 50 przepisania od zera. Używana jako mapa decyzyjna, a nie ocena jakości, daje kolejkę priorytetów: funkcje z wysokim CC, których dotknie twój następny feature, to twój prawdziwy plan sprintu.

Wizualizacja złożoności cyklomatycznej w bazie kodu z kolorowym grafem przepływu sterowania

Otwierasz projekt o 22:00. Wiesz, co dalej chcesz shipować. Po dwudziestu minutach debugujesz coś zupełnie innego. Funkcja, którą edytujesz, ma 300 linii, obsługuje sześć różnych przypadków i nikt, łącznie z tobą, nie pamięta, skąd się wzięła. Złożoność cyklomatyczna to metryka, która wykryłaby tę funkcję wiele miesięcy temu. Zlicza niezależne ścieżki wykonania w kodzie. Funkcje powyżej CC 10 wymagają testów, zanim je dotkniesz. Powyżej CC 20 refaktorujesz przed dodaniem czegokolwiek nowego. Powyżej CC 50 przepisujesz od zera. To drzewo decyzyjne. Reszta artykułu pokazuje, jak uruchomić scan na twoim projekcie i zamienić wynik w plan sprintu.

Co tak naprawdę mierzy złożoność cyklomatyczna

Thomas McCabe opublikował tę metrykę w 1976 roku. Wzór to M = E - N + 2P, gdzie E to krawędzie, N to węzły, a P to spójne składowe w grafie przepływu sterowania funkcji. W praktyce: zaczynasz od 1 i dodajesz 1 za każdy if, else if, for, while, case, catch, && lub ||. To jest wynik.

Funkcja bez rozgałęzień ma CC = 1. Funkcja sprawdzająca dziesięć warunków, iterująca po liście i obsługująca trzy typy błędów osiągnie CC 15-20. Funkcja budowana przez sześciu dewów, którzy przez dwa lata łatali sobie nawzajem logikę, może dobić do CC 60.

Metryka nie mierzy wszystkiego. Nie mierzy głębokości zagnieżdżenia, jakości nazewnictwa ani klarowności abstrakcji. Funkcja może mieć niski CC i nadal być trudna do czytania. Ale funkcja z CC powyżej 25 jest prawie zawsze trudna do czytania. Korelacja w tym kierunku jest na tyle spójna, żeby służyć jako filtr priorytetów, nie jako ostateczna ocena jakości kodu.

Wizualizacja ścieżek rozgałęzień kodu z mapą ciepła złożoności

Progi, które decydują o twoim następnym kroku

Większość wytycznych i narzędzi do analizy statycznej zbiega się w podobnych przedziałach:

Pierwotna rekomendacja NIST to sufit 10 dla funkcji. W praktyce większość inżynierów traktuje 15 jako realistyczny miękki limit dla kodu produkcyjnego, a wszystko powyżej 20 jako blokadę dla nowych prac dotykających tej funkcji.

Tabela to twoje drzewo decyzyjne dla tego, co budować dalej. Kiedy backlog jest pełny i nie możesz zdecydować, czy dodać feature, czy naprawić istniejący moduł, najpierw uruchom scan złożoności. Funkcje siedzące powyżej 20 to twój obowiązkowy stop przed wszystkim, co nowe ich dotknie. To nie jest sugestia, to jest dane wejściowe do planowania.

Jak uruchomić pierwszą analizę w 5 minut

W każdym ekosystemie językowym jest gotowe narzędzie CLI. Nie potrzebujesz dashboardu SaaS do tego pierwszego kroku.

Python (Radon):

pip install radon
radon cc -s -a ./src

JavaScript i TypeScript (ESLint):

{ "rules": { "complexity": ["error", 10] } }

Następnie uruchamiasz eslint ./src --ext .ts,.js w pipeline.

Go (gocyclo):

go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
gocyclo -over 10 ./...

Java i Kotlin (PMD):

pmd check -d ./src -R rulesets/java/quickstart.xml | grep CyclomaticComplexity

Wynik to lista każdej funkcji posortowana według wyniku CC. Szukasz dziesięciu najwyżej ocenionych funkcji w projekcie. Zapisz tę listę. To twoja kolejka priorytetów na następny sprint. Nie potrzeba nic więcej do podjęcia pierwszej decyzji.

Jeśli twój projekt ma mniej niż 2000 linii, radon albo gocyclo skończy w kilka sekund. Jeśli przekracza 50 000 linii, rozważ uruchomienie scanu tylko na katalogu, w którym pracujesz aktualnie, żeby wynik był natychmiastowy i działał jako nawyk roboczy, a nie jednorazowy audyt.

Pierwszy scan ma jeden cel: znaleźć top 10 funkcji. Nie analizuj całego raportu przy pierwszym uruchomieniu. Zapisz listę, posortuj malejąco, zaznacz te, których dotknie twój następny feature. Wszystko inne możesz odłożyć na później.

Jak czytać raport złożoności jako mapę decyzyjną

Funkcje z wysokim CC skupiają się w przewidywalnych miejscach. W API rekomendacyjnym, liczącym około 8000 linii, uruchomienie radon cc -s wykazało trzy funkcje z CC powyżej 30. Wszystkie trzy znajdowały się w warstwie normalizacji danych. Ta warstwa była łatana przyrostowo przez sześć miesięcy, za każdym razem dla nieco innego edge case ze źródła danych. Nikt nigdy nie spojrzał na skumulowane szkody.

Każdy nowy feature dotykający warstwy normalizacji zajmował 40% dłużej do shipowania. Dwa dni przed kolejnym sprintem poświęciliśmy na refaktoryzację tych trzech funkcji, obniżając wyniki CC z 32, 28 i 31 do 7, 5 i 8. Następny sprint był najszybszy od czterech miesięcy.

Trzy funkcje. Dwa dni. Cztery miesiące opóźnień, wyjaśnione.

Wzorzec jest spójny w różnych bazach kodu: około 10-15% funkcji przechowuje 70-80% złożoności w każdym organicznie rozwijającym się projekcie. Ta koncentracja to twoja prawdziwa mapa. Przed zacommitowaniem się do nowego feature'a uruchom scan. Jeśli twój feature dotyka funkcji powyżej CC 15, masz konkretny wybór: shipować go teraz wolno w ryzykowny kod, albo poświęcić dwa dni na refaktoryzację i shipować go porządnie w następnym tygodniu. Matematyka zwykle faworyzuje refaktoryzację.

W side projektach ma to większe znaczenie niż w ustawieniach zespołowych. Nie możesz przekazać pogmatwanej funkcji współpracownikowi. Pełny koszt kognitywny ponownego zrozumienia kodu CC 30 spada na ciebie, zazwyczaj o 22:00, sześć miesięcy po tym, jak go napisałeś. Mając to w głowie, wybór między "dodaję feature" a "najpierw czyszczę" przestaje być subiektywny.

Deweloper analizujący metryki jakości kodu na dwóch monitorach w ciemnym miejscu pracy

Kiedy wysoka złożoność jest uzasadniona

Nie każda funkcja z wysokim CC jest problemem.

Maszyna stanów płatności obsługująca 12 stanów transakcji naprawdę musi obsługiwać 12 stanów. Parser przetwarzający 15 reguł gramatycznych ma 15 prawdziwych przypadków. Spłaszczenie ich do 15 helperów z CC 1 każdy nie redukuje złożoności, tylko ją rozprasza po plikach, które są teraz trudniejsze w nawigacji razem.

Użyteczne pytanie to nie "czy CC jest zbyt wysokie?", ale "czy CC jest wyższe, niż wymaga domena?" Funkcja routingu obsługująca 15 wzorców URL z CC = 18 jest prawdopodobnie właściwa. Funkcja aktualizacji profilu użytkownika z CC = 18, która urosła przez akumulowane łatki dla edge case'ów, których nikt nie planował, to inny problem.

Trzymaj CC poniżej 15 na krytycznych ścieżkach. Pozwól mu rosnąć wyżej w naprawdę złożonych domenach i dokumentuj powód inline. Funkcje, które później cię zaskakują, to zawsze te bez uzasadnienia dla swojej złożoności poza akumulowanymi poprawkami. To co kosztuje, to nie wysokie CC samo w sobie, lecz wysokie CC bez wyjaśnienia dlaczego musi być wysokie.

Dodaj kontrolę złożoności do CI i przestań debugować na ślepo

Uruchomienie scanu raz jest przydatne. Podłączenie go do CI to to, co naprawdę zmienia zachowanie w czasie.

Praktyczna konfiguracja: ustaw próg w konfiguracji lintera, niech build nie przejdzie, jeśli jakakolwiek nowa funkcja go przekroczy, i zapisuj raport złożoności jako artefakt CI przy każdym uruchomieniu. Lista istnieje przy każdym buildzie, bez żeby ktokolwiek pamiętał o jej generowaniu.

Dla GitHub Actions z projektem Python:

- name: Sprawdź złożoność
  run: |
    pip install radon
    radon cc -n C -s ./src
    if [ $? -ne 0 ]; then exit 1; fi

Dla JavaScript z ESLint już w pipeline, dodaj "complexity": ["error", 12] do reguł. Każdy PR wypychający funkcję powyżej 12 automatycznie nie przechodzi.

Praktyczny standard zespołowy: CC <= 15 jako twarda blokada CI, CC 11-15 jako ostrzeżenie wymagające komentarza inline wyjaśniającego powód biznesowy. Ten drugi poziom wymusza rozmowę bez zamieniania każdego code review w negocjację wyjątków od progu. W projekcie solo to samo ostrzeżenie działa jako punkt refleksji przed mergem.

Trzy narzędzia do bieżącego śledzenia złożoności

Narzędzia CLI powyżej dają jednorazową migawkę. Do bieżącej widoczności na poziomie projektu wyróżniają się trzy opcje:

SonarQube to najbardziej kompletna opcja do śledzenia złożoności na poziomie projektu. Edycja Community obejmuje złożoność cyklomatyczną, złożoność kognitywną i pokrycie testami w jednym dashboardzie. Quality gates mogą blokować merge, jeśli nowy kod przekracza próg złożoności. To narzędzie, po które sięgasz najpierw, gdy projekt ma więcej niż dwóch współtwórców i prawdziwy proces review.

CodeScene koreluje wyniki złożoności z historią commitów, wskazując funkcje zarówno złożone, jak i często zmieniane. Te przecięcia to twój prawdziwy dług techniczny, nie tylko trudne do zrozumienia, ale aktywnie kosztujące czas każdego sprintu. Jeśli musisz przedstawić argument za sprintem refaktoryzacji w roadmapie zespołu, wynik CodeScene jest tym argumentem.

Code Climate Quality to lżejsza opcja, odpowiednia dla projektów solo i repozytoriów open-source. Integracja z GitHub działa od razu po zainstalowaniu, ustawienia domyślne są sensowne, a bezpłatny tier dla publicznych repozytoriów jest użyteczny. Jeśli zarządzasz side projektem i chcesz śledzić dryfowanie złożoności bez stawiania własnej infrastruktury, zacznij tutaj.

Jaka decyzja naprawdę wymusza złożoność cyklomatyczna

Złożoność cyklomatyczna nie mówi ci, czy twój kod jest dobry. Mówi ci, gdzie ryzyko jest skoncentrowane. To jest bardziej użyteczna informacja.

Uruchom scan na bieżącym projekcie przed następnym sprintem. Spójrz na dziesięć najwyżej ocenionych funkcji. Zapytaj, które z nich dotknie twój następny feature. Jeśli odpowiedź brzmi "trzy z nich, wszystkie powyżej CC 20", twój plan sprintu jest jasny: sprowadź te trzy funkcje poniżej CC 10, potem shipuj feature. Zajmie to połowę czasu, który by zajął w przeciwnym razie.

Wniosek dla builderów: jeśli szukasz następnego side projektu i pracujesz w prawdziwych bazach kodu, twój raport złożoności jest specyfikacją. Najbardziej złożony moduł blokujący velocity, ten złożony i dotykany ciągle, to narzędzie warte zbudowania. Lekki dashboard złożoności korelujący wyniki CC z danymi git blame i częstotliwością zmienianych plików, wyceniony dla dwuosobowego zespołu, a nie dla kontrastu enterprise, to projekt, który jeszcze w pełni nie istnieje. Luka w narzędziach jest realna.

To nie pitch. To wzorzec: narzędzia, których chcesz, a nie możesz znaleźć, to często side projekty najbardziej warte shipowania.

Frequently asked questions

Jaki wynik złożoności cyklomatycznej jest dobry?
CC 1-10 jest ogólnie akceptowalne. Pierwotna rekomendacja NIST to sufit 10 dla funkcji. Wyniki 11-20 wymagają przeglądu i pokrycia testami przed modyfikacją. Wszystko powyżej 20 to silny kandydat do refaktoryzacji przed nową pracą dotykającą tego samego kodu.
Jak oblicza się złożoność cyklomatyczną?
Wzór to M = E - N + 2P, gdzie E to krawędzie, N to węzły, a P to spójne składowe w grafie przepływu sterowania. W praktyce: zacznij od 1 i dodaj 1 za każdy if, else if, for, while, case, catch, && i ||. Każde duże narzędzie do analizy statycznej robi to automatycznie.
Jak złożoność cyklomatyczna wpływa na side projekty?
W projektach solo wysokie CC kumuluje się szybko. Nie masz zespołu, z którym możesz podzielić się kontekstem. Funkcja napisana przy CC 30 cztery miesiące temu będzie kosztować dzień ponownego zrozumienia, zanim będziesz mógł ją bezpiecznie zmodyfikować. Trzymanie krytycznych ścieżek poniżej CC 15 to jedna z najskuteczniejszych inwestycji w przyszłą prędkość shipowania.
Czy złożoność cyklomatyczna to to samo co złożoność kognitywna?
Nie. Złożoność cyklomatyczna liczy niezależne ścieżki przez przepływ sterowania, co odpowiada minimalnej liczbie przypadków testowych. Złożoność kognitywna, opracowana przez SonarSource, waży głębokość zagnieżdżenia i przerwy przepływu sterowania, lepiej odzwierciedlając wysiłek umysłowy przy czytaniu kodu. Obie są przydatne, ale CC jest szerzej wspierane w narzędziach.
Kiedy przepisać zamiast refaktorować?
Gdy funkcja ma CC powyżej 50 i brak znaczącego pokrycia testami, bezpieczna refaktoryzacja wymaga testów, których nie możesz napisać, bo kod jest zbyt splątany do izolowanego testowania. Przepisanie z testami od zera jest często szybsze i bezpieczniejsze. Większość inżynierów umieszcza praktyczny punkt przegięcia między CC 40 a 60.
Jakie narzędzia mierzą złożoność cyklomatyczną?
Radon dla Python, reguła complexity w ESLint dla JavaScript i TypeScript, gocyclo dla Go, PMD dla Java, SonarQube dla wielojęzycznych projektów enterprise, Code Climate Quality dla lżejszej opcji wielojęzycznej, CodeScene dla złożoności skorelowanej z historią git. Większość platform CI integruje się z co najmniej jednym z tych narzędzi.
Czy złożoność cyklomatyczna może być sztucznie za niska?
Tak. Wyekstrahowanie każdego rozgałęzienia do jednoliniowego helpera obniża CC oryginalnej funkcji bez redukowania prawdziwej złożoności, tylko rozprasza ją po plikach. Funkcja z CC 1, która deleguje całe rozgałęzienie do nieprzejrzystych helperów, jest myląca. Używaj CC jako jednego sygnału obok przeglądu czytelności, nie jako liczby do minimalizowania za wszelką cenę.