Comme présenté brièvement dans mon article sur les bonnes pratiques en PHP (applicables à d’autres langages), éviter les « else » en programmation permet d’avoir un code + clair, + modulable.

Je donne des cours de PHP à My Digital School à Rennes (Campus de Ker Lann exactement). Ces exemples ci-dessous sont issus de mes cours. Je ne coderais pas tout l’exercice, seulement les parties liées aux if/else.

Le choix d’une chambre : Conditions enchainées

Un de mes exercices de base consiste à faire évoluer un personnage dans une aventure. Vous commencez avec plusieurs valeurs contenues dans différentes variables et vous devez passer certaines « épreuves ». L’une d’entre elles consiste à choisir une chambre dans une auberge selon vos points de vie et vos pièces d’or.

Voici les variables utiles :

$piecesDOr = rand(5, 15);
$pointsDeVie = rand(50, 100);

Et voici l’énoncé :

J’arrive à une auberge, je vais pouvoir me reposer (si j’ai des sous)
– La suite coûte 9 pièces d’or, et me rend 100% de mes PV manquants, +1 force, +1 defense
– La chambre double coûte 5 pièces d’or et me rend 90% de mes PV manquants, +1 defense
– La chambre simple coûte 3 pièces d’or et me rend 85% de mes PV manquants
– Libre à vous de le coder en mode « radin » ou en mode « bling bling »
– Si je n’ai plus d’or, je récupère seulement 30% de mes PV manquants

Une première façon de coder cette épreuve est de suivre simplement l’énoncé et d’enchainer les if / else / if / else …

if ($piecesDor >= 9) {
    // Je vais dans la Suite
    // Je récupère 100% de PV
    // Je gagne de la force et de la défense
} elseif ($piecesDOr >= 5) {
    // Je vais dans la chambre double
    // Je récupère 90% de mes PV manquants
    // Je gagne de la défense
} elseif ($piecesDOr >= 3) {
    // Je vais dans la chambre simple
    // Je récupère 30% de mes PV manquants
} else {
    // Aucune chambre, je dors dehors
    // Je récupère 30%  de mes PV manquants
}

Ici, la solution codée est très « primaire », elle répond à l’énoncé. Mais si on se trouvait dans un jeu vidéo par exemple, on ne se comporterait sans doute pas comme ça. Si j’ai 9 pièces d’or mais que j’ai tous mes points de vie, je ne vais pas dépenser 9 pièces d’or pour… rien ! Du coup, on peut le coder de manière différente, plus intelligente, en utilisant notamment un booléen pour savoir s’il y a eu une prise de décision ou non :

// Booléen qui mémorise ma prise de décision
$jAiChoisiUneChambre = false;

// Condition + intelligente
// J'en dépenserais 9, que si j'en ai au moins 12, et que si mes PV sont sous un certain seuil
if ($piecesDOr >= 12 and $pointsDeVie < 60) {
    // Je vais dans la Suite
    // Je récupère 100% de PV
    // Je gagne de la force et de la défense
    $jAiChoisiUneChambre = true;
}

// A partir de la seconde condition, je dois donc tester $jAiChoisiUneChambre
if (!$jAiChoisiUneChambre and $piecesDOr >= 7 and $pointsDeVie < 75) {
    // Je vais dans la chambre double
    // Je récupère 90% de mes PV manquants
    // Je gagne de la défense
    $jAiChoisiUneChambre = true;
}

// ...

// Dernier cas, si aucun autre n'a été réalisé
if (!$jAiChoisiUneChambre) {
    // Je récupère 30%  de mes PV manquants
}

Cette fois-ci, aucun else ! De cette manière, chaque condition de choix est dans un bloc de code dédié. Il devient beaucoup plus aisé d’en changer l’ordre, pour par exemple commencer à tester la chambre simple et non la suite, etc. Avec le booléen qui mémorise la condition, on s’assure qu’il n’y aura bien qu’un seul bloc d’exécuté. On retrouve également un code + lisible avec une dernière condition très explicite, qui peut se lire « Si je n’ai pas encore choisi de chambre ».

Par rigueur, on pourrait ajouter le test du booléen dès la première condition pour ne pas avoir de soucis si on intervertit les blocs et qu’on oublie de l’ajouter.

Marchand et négociation : instructions répétées

Dans le même projet, cette fois-ci notre aventurier rencontre un marchand. Si notre aventurier est rusé, il va négocier pour acheter la Hache du marchand, sinon il achètera un objet quelconque. Mais la hache coute + chère et le marchand n’aime pas perdre son temps !

Les variables utiles :

$ruse = rand(0, 10);
$piecesDOr = rand(0, 10);

Et l’énoncé :

Je rencontre un marchand, il me propose deux objets. Si ma ruse est de 4 ou moins, alors il me vend l’objet ‘Epine dorée’ pour 1 piece d’or, sinon il me vend l’objet ‘Hache’ pour 5 pièces d’or

Si je n’ai pas assez d’or (pour l’un ou l’autre des objets) et que j’ai essayé de l’entourlouper, il me frappe et je perds 20 points de vie.

De la même manière, voici une première possibilité de coder cette épreuve, avec des if else imbriqués :

// On commence par tester la ruse
if ($ruse <= 4) {
    // Je ne suis pas très rusé, je vais acheter l'épine dorée
    if ($piecesDOr >= 1) {
        // Je dépense 1 pièce
        // Je récupère l'objet Epine dorée
    } else {
        // Je perds 20 PV
    }
} else {
    // Je suis rusé, je vais acheter la hache
    if ($piecesDOr >= 5) {
        // Je dépense 5 pièces
        // Je récupère l'objet Hache
    } else {
        // Je perds 20 PV
    }
}

Là, on commence à imbriquer les conditions… Si je venais à changer l’énoncé pour ajouter un troisième objet à acheter par exemple, ça pourrait être compliqué de mettre à jour le code.

Essayons à nouveau, sans else, avec le même principe de booléen :

// Booléen qui se rappelle de l'action
$jAiAcheteUnObjet = false;

// Je peux grouper mes conditions, avec le test du booléen dès le début
// Hache
if (!$jAiAcheteUnObjet and $ruse > 4 and $piecesDOr >= 5) {
    // Je dépense 5 pièces
    // Je récupère l'objet Hache
    // Je mets à jour le booléen
    $jAiAcheteUnObjet = true;
}

// Epine dorée
if (!$jAiAcheteUnObjet and $ruse <= 4 and $piecesDOr >= 1) {
    // Je dépense 1 pièce
    // Je récupère l'objet Epine dorée
    // Je mets à jour le booléen
    $jAiAcheteUnObjet = true;
}

if (!$jAiAcheteUnObjet) {
    // Je perds 20 PV
    // Plus de répétition de cette instruction !
}

Ce code peut paraitre + long mais il est + clair et + facile à maintenir. Je peux facilement le modifier pour ajouter un troisième cas, à n’importe quel endroit. Et mon action de « perdre 20 PV » n’est plus répétée.

Méthode d’un objet : le return anticipé

Une fois que mes étudiants sont plus avancés dans leur cours, on passe à la partie Objet de la programmation PHP.

Cette fois-ci l’énoncé est un peu + complexe, on travaille sur un objet Heros qui représente un personnage de jeu vidéo. Ce personnage peut infliger des dégats en fonction de sa catégorie.

Les attributs intéressants :

class Heros
{
    public $munitions; // (int) Consommable
    public $capaciteDeDegats; // (int) Degats "infligeables"
    public $classeDuHeros; // (string) nom de la classe
}

Et l’énoncé :

– 1pt de dégat coute 1pt de munition
– Les dégats sont infligés si les munitions sont suffisantes
– Si j’ai une capacité de dégats de 45, je dois donc avoir au moins 45 munitions
– Spécial, pour un Heros de classe Degat, 1pt de dégat coute seulement 0,5pt de munition

Encore une fois, codons ça avec des if / else

public function degats()
{
    // Je calcule combien de munitions ça va me couter
    if ($this->classDuHeros == CLASS_DEGAT) {
        $coutDesDegats = $this->capaciteDeDegats / 2;
    } else {
        $coutDesDegats = $this->capaciteDeDegats;
    }
    
    // Je dois avoir assez de munitions
    if ($coutsDesDegats >= $this->munitions) {
        $this->munitions -= $coutsDesDegats;
        return $this->capaciteDeDegats;
    } else {
        return 0;
    }
}

Il ne faut pas oublier qu’un return « termine » une méthode. Aucun code situé après un return exécuté ne sera pris en compte. On peut donc avoir quelque chose comme ça :

public function degats()
{
    // Pourquoi pas tester directement certains éléments
    if ($this->munitions == 0 or $this->capaciteDeDegats == 0) {
        return 0;
    } // Pas besoin de else...

    // On initialise et, seulement si besoin, on altère la valeur
    $coutDesDegats = $this->capaciteDeDegats;
    if ($this->classDuHeros == CLASS_DEGAT) {
        $coutDesDegats /= 2;
    }

    // Je dois avoir assez de munitions
    if ($coutsDesDegats >= $this->munitions) {
        $this->munitions -= $coutsDesDegats;
        return $this->capaciteDeDegats;
    }

    // En fait pas besoin de else, si on a pas réussi à faire un return avant, c'est qu'il ne nous reste "plus" qu'à return 0 !
    return 0;
}

Ici, il faut bien comprendre pourquoi le else est superflu. Dans la première condition, si une des valeurs testées est à 0, alors on retourne 0 et ce sera la « fin » de la méthode. Rien d’autre ne sera exécuté. Au contraire, si aucune valeur n’est à 0, on ne retourne rien, donc on continue la suite de la méthode.

Rappel : pour la classe du héros, on teste une chaine de caractères précise. J’ai donc fait appel à une constante, en accord avec mes bonnes pratiques PHP 😉

Conclusion

Mes clés pour éviter les else :

  • Utiliser des booléens pour savoir s’il y a toujours une action à exécuter ou non plutot que des if/else imbriqués
  • Utiliser le return astucieusement pour « finir » une fonction

Attention, il y a des cas où le else est toujours utile. Il n’est pas toujours nécessaire de se « forcer » à l’enlever. Mais dans de nombreux cas, avec un peu de pratique de ces méthodes, on se rend compte qu’il n’est pas si utile et que le code devient + agréable sans lui !

Bon code à tous 🙂

Publié par Arthur Weill

Directeur de l’agence rennaise Web and Cow, directeur digital du Groupe Valorex, je suis ingénieur généraliste de formation, diplômé de l’école lilloise HEI.