Complexitatea cognitiva si complexitatea cyclomatica

Clean Code | Stefanescu Mihai | 2022-12-11

Atat timp cat scriem cod in mod profesional, metricile sunt foarte importante pentru a determina daca scriem cod de calitate ce este usor de testat, inteles si intretinut pe termen lung. Cum fiecare dezvoltator are stilul lui de a scrie devine important sa setam un standard dupa care sa masuram daca acel cod este sau nu de calitate. Doua dintre cele mai relevante metrici pe care ar trebui sa le avem in calcul sunt complexitatea cognitiva si cyclomatica.

Complexitatea Cyclomatica si fratele ei mai mic, Complexitatea Cognitiva

Conceptul de complexitate cognitiva a fost adus de SonarQube. Ai vrut sa introduca un mod de masurare a complexitati codului mai contextual. Stiu, exista mai multe surse din care puteti citi despre complexitatea cognitiva, inclusiv acest whitepaper de la sonar. In schimb, in acest articol am sa incerc sa povestesc cat mai simplu ca sa fie cat se poate de usor de inteles.

Complexitatea Cyclomatica

Aceasta metrica masoara cat de greu este de testat codul. Mai exact, calculeaza numarul de test case-uri distincte de care ai nevoie pentru a avea acoperire de 100% in testele unitare.

Complexitatea Cognitiva

Masoara cat de greu este de citit si inteles codul.
Hai sa incerc sa explic cu o bucata de cod:

switch ($input) {
	case 1:
		// rulam o anumita logica de business
		break;
	default:
		// rulam alta logica de business
		break;
}

Complexitatea cyclomatica a acestui cod este de 2. Ajungem la aceasta concluzie pentru ca ai 2 testcase-uri, cate unul pentru fiecare case din switch. Desi acest exemplu este simplist, extrapoland putem intelege ca impreuna cu restul codului din functia in care este acest switch si codul din case-uri acest rezultat poate creste in mod liniar.
Am sa las mai jos un tabel cu un guideline al complexitatilor si riscurilor modificarii codului ulterior:

Scor Complexitate Cyclomatica Risk
de la 1 la 10 Simpla Fara riscuri
de la 11 la 20 Complexa Anumite riscuri
de la 21 la 50 Mult prea complexa Risc mediu, necesita atentie
mai mult de 50 Mult prea complexa Prea multe cazuri de testare, risc mare

Desi avem o complexitate cyclomatica de 2, complexitatea cognitiva pentru acest cod este de 1 (sa ne amintim ca complexitatea cognitiva indica cat de usor de inteles este codul), deci sa citesti un switch cu 2 case-uri este relativ usor de citit si inteles.

Hai sa mai luam cateva exemple care sa ne ajute sa intelegem mai bine diferenta dintre cele 2. Am sa iau ca exemplu un program simplu care va decide daca un input dat este sau nu un an bisect sau nu.
Pentru a decide daca un an este bisect sau nu trebuie sa vedem daca este divizibil cu 4, de asemenea, daca este divizibil cu 100, dar nu cu 400, iar apoi, daca este divizibil cu 400.

Testaces-urile noastre sunt urmatoarele:

An bisesc : 1980, 1996, 2000, 2400 etc.

An care nu este bisect : 1981, 1995, 1700, 1900 etc.

Prima varianta a acestui cod:

public function  isLeapYear(int $year)
{
	$response = false;

	if ($year >= 1000 && $year <= 9999) {
		// anul este divizibil cu 400, ex: 1600, 2000, etc
		if ($year % 400 == 0){
			$response = true;
		// anul este divizibil cu 100, dar nu cu 400, ex: 1700, 1900, etc
		} elseif (($year % 100 == 0) && ($year % 400 != 0)) {
			$response = false;
		// in cele din urma, daca primele 2 conditii nu sunt indeplinite
		// verificam daca este divizibil cu 4
		}elseif ($year % 4 == 0) {
			$response = true;
		}
	} else {
		throw new Exception("Anul data ca parametru trebuie sa fie intre 1000 si 9999");
	}

	return $response;
}

Complexitatea cyclomatica pentru aceasta functie este de 7, decide asta uitandu-se la fiecare bucata de cod subliniata cu rosu din imaginea de mai jos

In schimb, pentru complexitatea cognitiva masuratoarea se face altfel:

Pentru fiecare conditie mai creste complexitatea cu 1, daca exista o despartire a conditiei, de exemplu && si || nou complexitatea este crescuta. Daca Avem 5 conditii ce contin &&, atunci scorul va fi de 1, in schimb  daca avem combinatii de && si ||, pentru fiecare dintre ele scorul va fi crescut.

Deci, ce putem face pentru a scade acest scor? Daca ne uitam mai atent la cod, observa, ca avem operatii care se repeta si in urma carora luam o decizie. Deci, daca facem o cunftie noua checkDivisibility() care sa contina aceasta logica, am putea reduce numarul de conditii.

pubic function checkDivisibility(int $year, int $dividedBy, bool $cont) {
	$response = false;
    // ne folosim de $cont pentru a determina daca conditia precedenta a fost indeplinita
    // si mergem mai departe doar daca a fost indeplinita
	if ($cont) {
		$remainder = $year % $dividedBy;
		if ($remainder == 0) {
			$response = true;
		}
	}
	return $response;
}

Acum, in functia care determina daca anul este sau nu bisect putem face urmatoarele modificari:

public function  isLeapYear(int $year)
{
	$response = false;

	if ($year >= 1000 && $year <= 9999) {
		$isDivisibleByFour = $this->checkDivisibility($year, 4, true);
		$isDivisibleByOneHundred = $this->checkDivisibility($year, 100, $isDivisibleByFour);
		$isDivisibleByFourHundred = $this->checkDivisibility($year, 400, $isDivisibleByOneHundred);

		$response = ($isDivisibleByFour && !($isDivisibleByOneHundred)) || $isDivisibleByFourHundred;
	} else {
		throw new Exception("Anul data ca parametru trebuie sa fie intre 1000 si 9999");
	}

	return $response;
}

Acum, putem observa cum am reusit sa reducem complexitatea cyclomatica (liniile rosii) cat si pe cea cognitiva (liniile verzi)

Ba chiar mai mult, putem reduce complexitatea cyclomatica si mai mult mutand verificarea range-ului de ani (1000 - 9999) intr-o functie separata.

public function validateRange(int $year)
{
	if ($year >= 1000 && $year <= 9999) {
		return true;
	}
	
	return false;
}

iar functia care principala va deveni:

public function  isLeapYear(int $year)
{
	if (!$this->validateRange($year)) {
		throw new Exception("Anul data ca parametru trebuie sa fie intre 1000 si 9999");
	}

	$isDivisibleByFour = $this->checkDivisibility($year, 4, true);
	$isDivisibleByOneHundred = $this->checkDivisibility($year, 100, $isDivisibleByFour);
	$isDivisibleByFourHundred = $this->checkDivisibility($year, 400, $isDivisibleByOneHundred);

	return ($isDivisibleByFour && !($isDivisibleByOneHundred)) || $isDivisibleByFourHundred;
}

Concluzie
1. Incearca sa scrii metode cat mai mici care sa faca un singur lucru.
2. Profita de utilitatile pe care ti le pune la dispozitie limbajul in care lucrezi.


Imi place ce faci aici
Daca iti place ce fac aici imi poti cumpara o cafea Buy Me a Coffee at ko-fi.com

Stefanescu Mihai
Programator de ~8 ani, am lucrat la proiecte din mai multe industrstrii, de la eCommerce la telecomunicatii la automatizari. In acest timp am folosi diferite tehnologii, de la PHP, MySQL, PostgreSql, RabbitMq, Redis, Memcached si altele.
       

Get in touch
Pentru nelamuriri, dubii, comentarii si chestii de pe suflet ne putem auzi pe Discord, Reddit sau poti deschide o discutie noua pe forum.

Posteaza un comentariu

Comentarii

Inca nu au fost postate comentarii, fii primul care posteaza un comentariu!

Club-ul este dedicat membrilor si ofera access la mai multe zone ale website-ului.

🗝ïļ Login 🌟 Register

🔖 Bookmarks ⊞
âœĻ Pentru a sustine aceasta comunitate am sa te rog sa te autentifici sau sa te inregistrezi!

🌊ïļ Discord ⊞

Folosim cookie-uri pentru a oferi functionalitatile critice ale aplicatiei Invata-Programare. Folosim cookie-uri si pentru a analiza traficul, pentru care e nevoie de consimtamantul dvs. explicit.

⮆ïļ