PHP

Bonnes pratiques PHP #4 se passer des else… et des if

Avec quelques principes de programmation, on peut se passer complètement des else… voir des if.

La structure conditionnelle if/else est une structure très utilisée et est présente dans tous les langages de programmation.

Cependant, retirer les else, voir les if de ta syntaxe est un très bon moyen, à la fois d’organiser ton code et d’en améliorer la compréhension.

Pour illustrer tous ces exemples, j’utiliserais une classe Goldorak, qui mettra en scène le célèbre robot de l’espace piloté par Actarus. Pour les plus jeunes qui ne connaîtraient pas ou pour les nostalgiques, voici le générique.

final class Goldorak
{
	private int $munitions = 1000;
	
}

Initialisation

Quand Goldorak se fait attaquer, il doit choisir une arme. Il va la choisir en fonction de son ennemi. On peut donc avoir cette méthode dans le code :

public function choixDUneArme(string $ennemi): string
{
	if ($ennemi === 'navette') {
		$arme = 'laser';
	} elseif ($ennemi === 'golgoth') {
		$arme = 'poing';
	} elseif ($ennemi === 'monstrogoth') {
		$arme = 'hache';
	} elseif ($ennemi === 'navette amirale') {
		$arme = 'cornes';
	}

	// Si je n'ai pas encore choisi
	if (!isset($arme)) {
		$arme = 'laser';
	}

	return $arme;
}

Là je remarque que j’ai 2 fois ‘laser’ et que j’ai après mes if/else/if un if(!isset) qui me permet de définir une valeur même si je n’ai rencontré aucune de mes conditions.

Je peux donc ici initialiser $arme à ‘laser’ pour gagner déjà quelques lignes et avoir quelque chose de + propre :

public function choixDUneArme(string $ennemi): string
{
	$arme = 'laser';

	if ($ennemi === 'golgoth') {
		$arme = 'poing';
	} elseif ($ennemi === 'monstrogoth') {
		$arme = 'hache';
	} elseif ($ennemi === 'navette amirale') {
		$arme = 'cornes';
	}

	return $arme;
}

J’ai déjà retiré un elseif et j’ai retiré ce isset à la fin.

Return anticipé

Ce qu’on oublie quand on commence à coder, ou qu’on ne nous dit pas toujours… c’est que return, en plus de renvoyer une valeur, termine la fonction ou méthode. C’est à dire que tout le code qui se trouve après n’est pas exécuté.

Je peux donc continuer à optimiser mon code, mon cas ‘laser’ va repasser à la fin :

public function choixDUneArme(string $ennemi): string
{
	if ($ennemi === 'golgoth') {
		return 'poing';
	}

	if ($ennemi === 'monstrogoth') {
		return 'hache';
	}

	if ($ennemi === 'navette amirale') {
		return 'cornes';
	}

	return 'laser';
}

Tous mes else ont disparu ! Ma variable $arme a disparu aussi. Elle ne servait en fait qu’à conserver la valeur qui serait retournée. Comme on fait les return directement, plus besoin de $arme.

Switch

Je peux remplacer ces if par un switch.

public function choixDUneArme(string $ennemi): string
{
	switch ($ennemi) {

		case 'golgoth':
			return 'poing';


		case 'monstrogoth':
			return 'hache';


		case 'navette amirale':
			return 'cornes';


		default:
			return 'laser';

	}
}

Les return permettent de ne pas avoir à mettre les instructions « break » dans chaque « case ».

Personnellement, je n’utilise pas le switch tant que ça dans mon code.

Match

PHP 8 amène une nouvelle structure de code : match

Voici donc le code ci-dessus écrit avec match :

public function choixDUneArme(string $ennemi): string
{
	return match ($ennemi) {
		'golgoth' => 'poing',
		'monstrogoth' => 'hache',
		'navette amirale' => 'cornes',
		default => 'laser'
	};
}

match permet de faire des choses + complexes que cet exemple. N’hésite pas à découvrir la documentation dédiée.

Tableau de correspondance

Dans mes exemples ci-dessus, j’ai perdu de vue de mon code la correspondance entre « navette » et « laser ». Ca n’a pas d’importance pour la bonne exécution de mon code, mais ça peut en avoir pour sa bonne compréhension.

On peut encore enlever nos if et avoir quelque chose de + détaillé avec un tableau de correspondance :

public function choixDUneArme(string $ennemi): string
{
	$choixDesArmes = [
		'navette' => 'laser',
		'golgoth' => 'poing',
		'monstrogoth' => 'hache',
		'navette amirale' => 'cornes'
	];
	
	if (isset($choixDesArmes[$ennemi])) {
		return $choixDesArmes[$ennemi];
	}

	return 'laser';
}

J’ai retiré mes if… mais revoilà mon isset ! En effet, le isset sécurise le fait que la correspondance n’existe pas forcément dans le tableau.

Opérateur null coalescent

Derrière ce nom compliqué se cache un opérateur permettant de retirer ce isset.

On avait déjà l’opérateur ternaire « ? » qui permet de condenser l’écriture d’un if. L’opérateur null coalescent « ?? » permet de condenser l’écriture de if (isset) else :

public function choixDUneArme(string $ennemi): string
{
	$choixDesArmes = [
		'navette' => 'laser',
		'golgoth' => 'poing',
		'monstrogoth' => 'hache',
		'navette amirale' => 'cornes'
	];
	
	return $choixDesArmes[$ennemi] ?? 'laser';
}

Si $choixDesArmes[$ennemi] existe, c’est ça qui sera retourné, sinon ce sera « laser ». Tout ça en 1 ligne !

Boucle foreach : continue

On change de méthode.

Goldorak va se frotter à une série d’ennemis. Ces ennemis ont plusieurs caractéristiques, dont un « type ». Si ce type est « espace », alors Goldorak ne peut pas le combattre et l’esquive, sinon il l’affronte :

foreach ($ennemis as $ennemi) {
	if ($ennemi->type === 'espace') {
		$this->esquive($ennemi);
	} else {
		$this->arme = $this->choixDUneArme();
		$this->attaque($ennemi);
	}
}

A chaque itération, je vais donc réaliser l’une ou l’autre des actions, jamais les deux. Je peux ici utiliser un continue pour représenter ça :

foreach ($ennemis as $ennemi) {
	
	if ($ennemi->type === 'espace') {
		$this->esquive($ennemi);
		continue;
	}

	$this->arme = $this->choixDUneArme();
	$this->attaque($ennemi);
}

La complexité de mon code est diminuée parce que mes instructions importantes ne sont plus « poussées vers la droite » par le else. On gagne en lisibilité et en compréhension, pour identifier ce qui se passe souvent (attaque) de ce qui est une exception (esquive).

Rappel : c’est vraiment pour l’exemple, Goldorak n’esquive pas les ennemis !

Boucle foreach : break

Exprès, je repars du code non optimisé précédent :

foreach ($ennemis as $ennemi) {
	if ($ennemi->type === 'espace') {
		if ($this->munitions <= 0) {
			$this->fuite();
		} else {
			$this->esquive($ennemi);
		}
	} else {
		if ($this->munitions <= 0) {
			$this->fuite();
		} else {
			$this->arme = $this->choixDUneArme();
			$this->attaque($ennemi);
		}
	}
}

Quand on débute, on a tendance à produire ce genre de code, imbriquer les if/else if/else est une véritable passion. Quand on l’écrit, ça paraît assez clair. Mais quand on y revient + tard, c’est tout de suite moins clair… et très vite, avec beaucoup + d’instructions que dans cet exemple, on se retrouve avec ce genre de portions de code :

	// ...
	// ...
	// 25 lignes
	// ...

} else {
	// Mais à quoi correspond ce else ??
}

Dans mon exemple ici, dès que je n’ai plus de munition, je passe toujours par la méthode fuite. Et si on considère que la méthode fuite ne fera plus grand-chose de plus après son premier appel, on peut optimiser le code de cette façon :

foreach ($ennemis as $ennemi) {

	if ($this->munitions <= 0) {
		$this->fuite();
		break;
	}

	if ($ennemi->type === 'espace') {
		$this->esquive($ennemi);
		continue;
	}

	$this->arme = $this->choixDUneArme();
	$this->attaque($ennemi);
}

Dès que les munitions seront insuffisantes, Goldorak fuit, et on « casse » la boucle pour arrêter de parcourir les ennemis.

Encore une fois ici, j’ai très peu de code « poussé vers la droite » par if/else imbriqués. C’est vraiment une bonne façon de procéder, à savoir garder son code « le plus à gauche » possible.

Return 2 le retour

On change de méthode.

On va contrôler le type d’un ennemi avec une méthode dédiée :

public function ennemiDeLEspace(Ennemi $ennemi): bool
{
	if ($ennemi->type === 'espace') {
		return true;
	} else {
		return false;
	}
}

Facile ! On l’a vu + haut, j’enlève le else :

public function ennemiDeLEspace(Ennemi $ennemi): bool
{
	if ($ennemi->type === 'espace') {
		return true;
	}
	
	return false;
}

En fait, on peut faire encore mieux ! Je lis le code : « Si le type de l’ennemi est espace, alors je retourne true, sinon je retourne false ». Je peux le simplifier par « Je retourne le fait que le type de l’ennemi vaut espace » :

public function ennemiDeLEspace(Ennemi $ennemi): bool
{
	return ($ennemi->type === 'espace') ;
}

Je retourne directement la comparaison, car celle-ci vaut true ou false, selon la valeur de $ennemi->type.

Compteur et typage

Avant de combattre, Goldorak va compter ses ennemis :

foreach ($ennemis as $ennemi) {

	if ($ennemi->nom === 'navette') {
		$compteurNavettes++;
	}

	if ($ennemi->nom === 'golgoth') {
		$compteurGolgoths++;
	}

	if ($ennemi->nom === 'monstrogoth') {
		$compteurMonstrogoths++;
	}
}

En jouant avec les types, je peux retirer les if :

$compteurNavettes = 0;
$compteurGolgoths = 0;
$compteurMonstrogoths = 0;

foreach ($ennemis as $ennemi) {
	$compteurNavettes += (int) ($ennemi->nom === 'navette');
	$compteurGolgoths += (int) ($ennemi->nom === 'golgoth');
	$compteurMonstrogoths += (int) ($ennemi->nom == 'monstrogoth');
}

Je change donc le type du retour de ma comparaison. true va devenir 1, false va devenir 0.

Et j’incrémente à chaque fois la valeur. Sauf que quand j’incrémente 0, c’est comme si je n’avais rien fait, donc mon compteur ne change pas.

Conclusion et point d’attention

Ces techniques te permettront d’optimiser ton code tout en obtenant le même résultat. Mais attention, ce ne sont pas des règles absolues à utiliser dans tous les cas. Quand on a l’habitude, je pense que le else peut vraiment être évité dans 99% des cas. Mais le if reste utile dans de nombreuses situations. Et garde en tête que le plus important c’est d’avoir un code compréhensible, par toi, ton équipe, et ton « futur toi », celui qui se replongera dans ton code quelques jours/semaines/mois + tard 😉


Qui a codé ce superbe contenu ?


Ta newsletter chaque mois

Corrigés, challenges, actualités, veille technique... aucun spam.