Cyclomatic Complexity

Dzisiaj chciałbym opisać krótko jedną z, moim zdaniem, bardziej wartościowych metryk, które wylicza nam Sonar – Cyclomatic Complexity.

Cyclomatic Complexity

Formalne podstawy złożoności cyklomatycznej podał McCabe w 1976 roku w swoim artykule A complexity measure. Miara ta mówi nam o tym iloma różnymi ścieżkami może płynąć logika w jakiejś metodzie.

Jak liczy ją Sonar dla Javy

Sonar idzie trochę na łatwiznę jeśli chodzi o liczenie complexity i nie dokonuje analizy grafu metody jako takiej. Zamast tego definiuję grupę słów kluczowych języka, które complexity zwiększają. Słowa te to:

  • Instrukcje warunkowe: if, case (ale nie else i default)
  • Operatory warunkowe: &&, ||, ? (ale nie operatory NIE będące short-circuit)
  • Instrukcje pętli: for, while
  • Instrukcje związane z wyjątkami: catch, throw (ale nie finally)
  • Instrukcja return, ale pod warunkiem, że nie jest to ostatnia instrukcja w metodzie

Każda metoda “na wstępie” ma complexity równe jeden a każdorazowe wystąpienie któregoś z wyżej wymienionych słów kluczowych powoduje podwyższenie wartości o jeden.

Rodzaje complexity

Sonar policzy nam trzy rodzaje złożoności:

  • method complexity – jest to średnia z złożoności wszystkich metod w klasie
  • class complexity – jest to suma złożności wszystkich metod danej klasy
  • file complexity – jest to suma złożności w danym pliku źródłowym

Na co należy uważać

Po raz kolejny zaznaczę, że nie jestem fanem pisania pod metryki i zdaje sobie sprawę, że kilka rzeczy o których wspomnę za chwilę może być nieco pataologiczne, ale pokazanie owych patalogii jest jednym z celów tego wpisu.

Equals

Po pierwsze  – metoda equals potrafi wytworzyć spore complexity. Naprawdę spore. Trudno tutaj doszukiwać się sposobów na uproszczenia, bo po pierwsze każdy przypadek trzeba rozpatrzyć osobno (czy na pewno musimy porównywać wszystkie pola?) a i tak chyba każda próba zmniejszenia złożoności equalsa będzie zachaczać o jakąś hack’ologię a nie o prawdziwe upraszczanie kodu. Prosty przykład.

public class DummyPojo {

	private int first;
	private int second;
	private int third;

	/*
	 * Konstruktory, getery setery, hashCode ...
	 */

	@Override
	public boolean equals(Object obj) {			//1
		if (this == obj)						//1
			return true;						//1
		if (obj == null)						//1
			return false;						//1
		if (getClass() != obj.getClass())		//1
			return false;						//1
		DummyPojo other = (DummyPojo) obj;
		if (first != other.first)				//1
			return false;						//1
		if (second != other.second)				//1
			return false;						//1
		if (third != other.third)				//1
			return false;						//1
		return true;
	}
}

Complexity tej metody jest równe 13. Dla porównania metodę uznaję się za prostą, gdy jej complexity nie jest większe od 10. Jeżeli klasa ta zawierałaby pola będące referencjami do obiektów a nie prymitywami, to dochodziłaby jeszcze logika sprawdzania nulli.

Możemy wykonać tutaj bardzo smutną konwersję ifów na operatory logiczne (nie mogą to być operatory short circuit)

	@Override
	public boolean equals(Object obj) {			//1

		if(!(obj instanceof DummyPojo))			//1
			return false;						//1

		DummyPojo other = (DummyPojo) obj;
		return this == other &
                        first != other.first &
                        second != other.second &
                        third != other.third;
	}

Sprawdzenia nulla i zgodności typów nie możemy zawrzeć w ostatniej linijce, ze względu na to, że musimy korzystać z normalnych operatorów logicznych, także narazilibyśmy się na NullPointerException i ClassCastException – natomiast możemy skorzystać z instanceof, które za jednym zamachem sprawdzi i zgodność typów i nulla.

Oczywiście powyższy zapis jest bardzo słaby – ponieważ zawsze wykonają się wszystkie porównania – nawet jeżeli już po pierwszym wiemy, że obiekty nie są takie same. Sprawę załatwiłyby shortcircuit operatory, jednak nie możemy ich użyć, bo zwiększą nam complexity.

Kolejność operacji

Spójrzmy na poniższy kawałek kodu:

public boolean process(String param) {					// 1
	if (param != null) { 								// 1
		return processString(param); 					// 1
	}
	throw new NullPointerException("param is null");	// 1
}

Jak widać złożoność powyższej metody wg Sonara jest równa 4. Widać tutaj wyraźnie jak dalekie uproszczenia przyjmuje to narzędzie. Tak naprawdę możliwe są tylko dwie ścieżki, albo warunek będzie spełniony i zwrócona zostanie wartość, albo nie zostanie spełniony i poleci wyjątek. Wystarczy proste zamienienie kolejności operacji aby zmniejszyć complexity:

public boolean process(String param) {					// 1
	if (param == null) { 								// 1
		throw new NullPointerException("param is null");  					// 1
	}
	return processString(param);	// ten return nie zwiększa complexity
}

Rzućmy okiem na kolejny przykład:

public boolean process(String param) throws MyException {	// 1
	try{
		return processString(param); 						// 1
	}
	catch(NullPointerException e) {							// 1
		throw new MyException(e);							// 1
	}
	catch (IllegalArgumentException e) { 					// 1
		throw new MyException(e);							// 1
	}
}

Każde przepakowanie wyjątku aby go wyrzucić dalej spowoduje wzrost complexity o 2. Tutaj w zasadzie nie mamy zbyt dużego pola manewru, poza patologicznymi hackami: można przypisać wynik metody processString do zmiennej i zwrócić ją jako ostatnią instrukcję (return na końcu metody nie zwiększa complexity). Możemy też rzucanie przepakowanego wyjątku wyprowadzić do osobnej metody (co już jest w ogóle totalnym nieporozumieniem).

Podsumowanie

Cyclomatic complexity wydaje mi się jedną z przydatniejszych metryk. Dzięki niej możemy diagnozować “zapalne” rejony w kodzie, gdzie logika klasy jest mocno skomplikowana i być może należałoby się jej przyjrzeć. Takie skomplikowane klasy mają tendencję to jeszcze większej komplikacji z upływem czasu.  Przykładowo, załóżmy, że musimy dokonać zmiany w takim pogmatwanym kodzie. Próba zrozumienia takiego kawałka kodu może być bardzo trudna i zajmować dużo czasu. Stąd też często pojawia się pokusa, aby dostawić ifa w którym obsłużymy sytuację, która nas interesuje.

Cyclomatic complexity jest też podstawą dość ciekawego narzędzia jakim jest kwadrant feathersa. Narzędzie to pozwala wskazywać nam, które klasy warto poddać refaktoryzacji a na które być może szkoda czasu. O kwadrancie Feathersa prawdopodobnie napiszę jeszczę kiedyś.

Podobnie jednak jak w przypadku wszystkich innych metryk – ortodoksyjne patrzenie w cyferki nie przyniesie nam żadnych korzyści. W tym wpisie pokazałem kilka sposobów na które można oszukać Sonara i jak widać sposoby te wcale nie wpływają na poprawienie jakości kodu a wręcz przeciwnie.

3 Replies to “Cyclomatic Complexity”

    1. W zasadzie nie planowałem w najbliższym czasie wracać do tematów związanych z metrykami, ale w sumie czemu nie. Jeśli uda mi się znaleźć trochę czasu w tygodniu to postaram się przygotować taki wpis.

Comments are closed.