Accueil Tuto Astuces de Dev Web n°7 : Le reformatage ou refactoring de code
Astuces de Dev Web n°7 - Le reformatage ou refactoring de code

Astuces de Dev Web n°7 : Le reformatage ou refactoring de code

par Jérémy PASTOURET

Le reformatage du code est un vaste sujet dans le domaine du développement. Certains développeurs ont leur petites manies ; d’autres sont plus désorganisés, en mode fouillis. Tout dépend de la personnalité de chacun. Et puis quand on travaille en équipe, une charte commune émane naturellement. Même s’il y en a toujours qui ne sont pas d’accord, et se laissent aller au bout de quelques semaines.

Donc, dans cet article, je vais vous parler des petits manies de mon équipe et des bonnes pratiques en PHP. Vous allez aussi découvrir que PHPStorm est particulièrement intelligent, et qu’il permet d’effectuer des modifications de styles très rapidement. L’avantage, c’est de pouvoir récupérer un projet sur Github avec un style lourd et difficile à lire. Mais je vous parlerai de cette partie PHPStorm dans un prochain article.

Exemple de code à reformater, ou refactoring

Petit code moche

Je vais vous afficher le code dans un bloc (plus facilement copiable). Si vous avez envie de jouer le jeu et de faire l’exercice avec moi, vous pouvez récupérer mon code sur votre IDE favori, le tester et essayer de l’améliorer. C’est à dire le refactorer.

Mon projet se nomme LesEnovaZonePoor : il permet de créer des produits et de les ajouter dans un panier. Cette application PHP contient uniquement 2 fichiers :

  • Mon pseudo Controller qui se nomme Manager.php et qui appelle mes fonctions présentes dans ma classe Panier
  • Ma classe Panier

Le fichier Manager.php :

<?php

include_once "Panier.php";


$panier = new Panier();
$panier->addProduit("Raspberry Pi", "RPI", 37.90);
$panier->addProduit("Klim Domination", "KLIMDOM", 59.90);

$panier->addProduit("iPhone X", "IPHX", 1000);

$produit_from_panier = $panier->getProduitPrix("IPHX");

echo "Le prix de mon produit est ".$panier->getProduitPrix("IPHX")."<br />";


$panier->deleteProduit("IPHX");

$produit_from_panier = $panier->getProduitPrix("IPHX");

if(null != $panier->getProduitPrix("IPHX")){
    echo "Le prix de mon produit qui est toujours vivant est ".$panier->getProduitPrix("IPHX")."<br />";
}
else{
    echo "Il n'y a pas de produit dont le nom est IPHX"."<br />";
}

$prix = $panier->getPrixTotal();

echo $prix;

Le fichier Panier.php :

<?php 
class Panier 
{ 
    protected $produit; 
    public function __construct(){$this->produit = array();}

    public function getProduits(){
        return $this->produit;
    }

    public function addProduit($name, $code, $price)
    {
        array_push($this->produit, array('name'=>$name,'code'=>$code, 'prix'=>$price));
    }

    public function getProduit($code){
        for($i=0;$i<count($this->produit);$i++)
            if($code == $this->produit[$i]['code'])
                return $this->produit[$i];
        return null;
    }

    public function getProduitPrix($code){
        for($i=0;$i<count($this->produit);$i++)
            if($code == $this->produit[$i]['code'])
                return $this->produit[$i]['prix'];
        return null;
    }

    public function deleteProduit($code){
        for($i=0;$i<count($this->produit);$i++)
            if($code == $this->produit[$i]['code'])
                unset($this->produit[$i]);
    }

    public function getPrixTotal(){
        $prix = 0;
        for($i=0;$i<count($this->produit);$i++)
            $prix += $this->produit[$i]['prix'];

        return $prix;
    }
}

Commençons par les choses de base qui nous gênent.

Short array or long array ?

Short array or long array

Depuis que PHP a sorti les « short array », j’en suis devenu un maniaque et je les transforme le plus possible (apparement on y gagne aussi en performance). Si vous ne savez pas ce qu’est un short array, ne vous inquiétez pas, voici un exemple :

On va prendre le constructeur de notre classe Panier, qui initialise un tableau de produit.
Voici le code avant :

<?php 
class Panier 
{ 
    protected $produit; 
    public function __construct(){$this->produit = array();}
}

Et voici le code après :

<?php 
class Panier 
{ 
    protected $produit; 
    public function __construct(){$this->produit = [];}
}

Donc l’idée, c’est de transformer tout les array() en []. Et ça marche aussi avec des éléments à l’intérieur.
On peut prendre comme exemple la fonction d’ajout des produits :

<?php 
public function addProduit($name, $code, $price) 
{ 
    array_push($this->produit, array('name'=>$name,'code'=>$code, 'prix'=>$price)); 
}

Celle-ci devient :

<?php 
public function addProduit($name, $code, $price) { 
    array_push($this->produit, ['name'=>$name,'code'=>$code, 'prix'=>$price]);
}

J’ai supprimé le terme « array » et remplacé les parenthèses par des crochets.

Maintenant je vous propose de travailler sur la lisibilité de cette fonction. En effet, je trouve que ce n’est pas terrible à lire.

Un choix difficile : lowerCamelCase ou snake_case ?

Un choix difficile lowerCamelCase ou snake_case

Pour ma part, je préfère le lowerCamelCase que le snake_case. Pour ceux qui ne connaissent pas ces formats, le lowerCamelCase permet de mettre en minuscule le premier mot et d’appliquer une première lettre majuscule sur tout les mots qui se rajoutent à la variable. Tandis que le snake_case sépare les mot avec des underscores. On fait ça naturellement avec les noms de dossiers ou les noms de fichiers sur notre système d’exploitation favori. Pour la petite histoire du CamelCase, voici un lien Wikipedia.

Peut-on avoir plus d’informations dans le nom de la variable ?

Peut-on avoir plus d'informations dans le nom de la variable

Pour que vous et vos collègues puissiez obtenir plus d’informations à la lecture /modification de votre code, je vous conseille fortement de rajouter une première lettre devant toutes vos variables pour en indiquer le type.

Notre fonction d’ajout de produits s’y prête bien.

<?php 
public function addProduit($sName, $sCode, $nPrice) { 
    array_push($this->aProduit, ['name'=>$sName,'code'=>$sCode, 'prix'=>$nPrice]);
}

Vous remarquerez que c’est plus pratique à lire. Nous savons dès à présent que le code d’un produit est un « string » (une chaîne de caractères), que le prix est un numérique et que la propriété « produit » est un tableau de produits. Vous pouvez aussi utilisez la lettre « o » pour indiquer un objet, « f » pour un « float » et « b » pour un boolléen. Vous avez dû remarquer que j’utilise uniquement du lowerCamelCase, c’est à dire que je commence par une minuscule puis je mets une majuscule sur le prochain mot : par exemple la variable « Date Of Birth » devient $sDateOfBirth. En effet, je n’aime pas trop le snake_case qui donne ce look-là : $_s_date_of_birth. Après, je pense que c’est une habitude à prendre, donc ce choix vous est réservé.

En ayant recours à cette pratique vous devrez adopter le lowerCamelCase, sinon vous vous retrouverez avec des variables de ce style :

$n_price, $s_name,

Comment obtenir de beaux tableaux PHP ?

Comment avoir de beaux tableaux PHP

Je vous propose ensuite d’afficher joliment le tableau passé en paramètre array_push, comme ceci :

<?php 
public function addProduit($sName, $sCode, $nPrice) { 
    array_push($this->aProduit,
        [
            'name'=>$sName,
            'code'=>$sCode,
            'prix'=>$nPrice
        ]
    );
}

C’est la première étape, mais je pense qu’on peux faire encore mieux en ajoutant une tabulation au niveau des éléments du tableau.

Ce qui donne :

<?php 
public function addProduit($sName, $sCode, $nPrice) { 
    array_push($this->aProduit,    
        [
            'name'    =>    $sName,
            'code'    =>    $sCode,
            'prix'    =>    $nPrice
        ]
    );
}

Concentrez-vous sur les noms de la fonction, des paramètres et des indices du tableau employés dans ce code. Y a-t-il quelque chose qui vous choque ?
Réponse : il y a un problème de cohérence. Un index du tableau est en français : « prix ». Sauf que le reste est en anglais. Le tableau quant à lui s’appelle Produit. Et pour le nom de la fonction, c’est pire, car on a un mélange anglais-français avec « addProduit ». Il faut donc faire un choix en équipe : soit écrire tout en anglais, soit tout en français. Et ça se discute aussi pour les commentaires.

Dans notre cas, il y a beaucoup plus d’anglais que de français. Par conséquent on migre tout en anglais, ce qui produit le code suivant :

<?php 
public function addProduit($sName, $sCode, $nPrice) { 
    array_push($this->aProduit,    
        [
            'name'    =>    $sName,
            'code'    =>    $sCode,
            'price'    =>    $nPrice
        ]
    );
}

C’est beau d’avoir des signes « égal » alignés ensemble

C'est beau d'avoir des égales alignés ensemble

Est-ce nouveau ou perturbant pour vous ?
Vous risquez de dire que je suis maniaque, et je vous comprends. Car au début, je n’étais pas comme ça ! C’est un collègue de boulot qui m’a transmis le virus. Et maintenant, quand les signes « égal » ne sont pas alignés par rapport aux variables proches, ça ne me plaît pas. Notre fonction ci-dessus est encore un bon exemple, voici ce qu’elle devient :

<?php 
public function addProduit($sName, $sCode, $nPrice) { 
    array_push($this->aProduit,    
        [
            'name'    =>    $sName,
            'code'    =>    $sCode,
            'price'   =>    $nPrice
        ]
    );
}

C’est plus sympa, non ? Cela marche aussi avec des variables. Par exemple, imaginons qu’on récupère les données d’un formulaire passé en méthode « POST ». Côté PHP, on devrait avoir quelque chose qui ressemble à ça :

<?php
$sFirstname = $_POST['firstname'];
$sLastname = $_POST['lastname'];
$sMail = $_POST['mail'];
$sPhone = $_POST['phone'];
$sDateOfBirth = $_POST['date_of_birth'];
$sAddress = $_POST['address'];
$sCity = $_POST['city'];
$sZipCode = $_POST['zipcode'];

Vous préférez donc la version  ci-dessus ou la version ci-dessous, en termes de visibilité ? (sans parler de la praticité pour le développeur à aligner les « égal »).

<?php
$sFirstname      = $_POST['firstname'];
$sLastname       = $_POST['lastname'];
$sMail           = $_POST['mail'];
$sPhone          = $_POST['phone'];
$sDateOfBirth    = $_POST['date_of_birth'];
$sAddress        = $_POST['address'];
$sCity           = $_POST['city'];
$sZipCode        = $_POST['zipcode'];

Après c’est un choix de style de code. Si la question du temps passé vous inquiète, je vous arrête tout de suite car dans le prochain article, je vous montrerai comment le faire en 2 secondes avec PHPStorm.

Le typage des paramètres et des retours de fonctions en PHP 7

Le typage des paramètres et des retours de fonctions en PHP 7

Une autre façon d’améliorer la lecture du code consiste à rajouter le typage des paramètres des fonctions introduites grâce à PHP 7. Ne vous inquiétez pas, on va y aller doucement. Je vous propose de partir sur une nouvelle fonction d’exemple : j’ai nommé le getProduct. Pour rappel, elle a cette tête-là :

<?php
public function getProduct($sCode){
    for($i=0;$i<count($this->aProduct);$i++)
        if($sCode == $this->aProduct[$i]['code'])
            return $this->aProduct[$i];
    return null;
}

En rajoutant le type de paramètre, on obtient le résultat suivant :

<?php
public function getProduct(string $sCode){
    for($i=0;$i<count($this->;aProduct);$i++)
        if($sCode == $this->aProduct[$i]['code'])
            return $this->aProduct[$i];
    return null;
}

Si votre IDE ne le reconnait pas, c’est que vous avez défini dans les paramètres de votre projet que la version de PHP utilisée est < 7.0. Si vous ne trouvez pas, je vous le montrerai dans un prochain article sur PHPStorm et le reformatage de code.

Concernant le typage du paramètre, vous pouvez aussi mettre « array », « object », « Product » (oui le nom d’une classe ça marche aussi). Cette façon d’écrire du code a deux avantages : la première, c’est que lorsque vous appelez cette fonction, votre IDE peut vous spécifier ce que la fonction attend comme paramètres. Si vous n’avez pas les bons types, votre IDE souligne la ligne en rouge et vous avertit du problème. Si vous connaissez un collègue qui risque de mal utiliser votre fonction, il se fera engueuler par PHP lorsque qu’il affichera la page. Car PHP vous informera sur votre page que le type passé ne correspond pas au type attendu. Donc pour conclure, toujours plus de visibilité et de vérification.

On peut aller encore plus loin en ajoutant le type retourné par la fonction. Voici le résultat sur notre précédente fonction :

<?php
public function getProduct(string $sCode):?Produit{
    for($i=0;$i<count($this->aProduct);$i++)
        if($sCode == $this->aProduct[$i]['code'])
            return $this->aProduct[$i];
    return null;
}

Ce qui peut paraître bizarre au début… mais on s’y fait.
Petite explication et détails pour typer les retours de fonction :
– il faut ajouter « : » après la parenthèse de fin de votre fonction.
– le « ? » signifie que la fonction peut retourner null ou le type indiqué, dans notre cas Produit.
– tout comme le typage des paramètres, vous avez les mêmes possibilités.

C’est plus stylé sans accolade !?

C'est plus stylé sans accolade !

Je n’en ai pas fini avec cette fonction getProduct qui me pose encore problème. Vous devez savoir qu’en PHP les accolades ne sont pas obligatoires dans certain cas. Par exemple dans getProduct, le for et le if n’en ont pas besoin car ils exécutent une seule instruction chacun. A partir du moment où il y en a plusieurs, il faut mettre des accolades. Alors quand on code nous-mêmes ce genre de choses, on trouve ça super clair et on en est fiers. Mais quand on le lit dans un code qu’on n’a pas écrit, c’est autre chose. C’est la raison pour laquelle je vous conseille fortement de mettre des accolades PARTOUT.

Toujours pas convaincu ? Je peux vous dire que de nombreux bug ont été générés à cause de ça. Un développeur qui ne voit pas qu’une instruction principale est dans un if – ou pas – peut modifier des valeurs issues d’un calcul ou autre… et causer des dommages importants. De nombreuses histoires existent sur le sujet : on a perdu beaucoup d’argent, voire même des vies humaines avec ce genre d’erreurs. Allez jeter un œil sur cette page wikipedia : Mariner_1.

Pour en revenir au code, voici le rendu en rajoutant les accolades :

<?php
public function getProduct(string $sCode):?Produit{
    for($i=0;$i<count($this->aProduct);$i++)
    {
        if($sCode == $this->aProduct[$i]['code'])
        {
            return $this->aProduct[$i];
        }
    }
    return null;
}

Pour en finir sur ce sujet, il est important de faire un choix commun avec l’équipe concernant le positionnement des accolades sur les fonctions.

Option 1 : Avec retour à la ligne

<?php
public function getProduct(string $sCode):?Produit
{
    for($i=0;$i<count($this->aProduct);$i++)
    {
        if($sCode == $this->aProduct[$i]['code'])
        {
            return $this->aProduct[$i];
        }
    }
    return null;
}

Option 2 : Sans retour à la ligne

<?php
public function getProduct(string $sCode):?Produit{
    for($i=0;$i<count($this->aProduct);$i++)
    {
        if($sCode == $this->aProduct[$i]['code'])
        {
            return $this->aProduct[$i];
        }
    }
    return null;
}

Vous allez me dire que ce’nest pas grand chose, mais je peux vous dire que Git vous remerciera, et que ça vous évitera de nombreux conflits de code.

Les commentaires ça sert à quelque chose ?

Les commentaires ça sert à quelque chose

Oui, les commentaires servent à quelque chose. Sur ce sujet, il y a plusieurs tendances dans le mode du dev. Selon les hard codeurs (je crois qu’on les appelle comme ça), pas de commentaire du tout, c’est mieux. Cela veut dire qu’ils codent tellement bien et clairement qu’il n’y a pas besoin de commentaire pour comprendre le code. Alors peut-être que je ne code pas aussi bien qu’eux. Mais je sais qu’en fonction des cas, on est obligé de mettre des commentaires… surtout si on doit développer un algo costaud.

Donc tout est une question de dosage. Vous devez en mettre là où ce n’est pas clair, voire vous poser des questions et améliorer le code par la suite. Il faut aussi se mettre d’accord avec l’équipe sur la rédaction en anglais ou en français des commentaires. Cela varie en fonction des projets : il y aura-t-il des devs étrangers ? Si oui, il faudrait partir sur l’anglais. Sinon optez plutôt pour le français car les commentaires seront plus riches (on a plus l’habitude de décrire en français qu’en anglais).

Petite astuce, vous pouvez utiliser certains mots-clés intéressants en PHP. Je vous présente le « TODO » qui permet d’indiquer au dev (ou à soi-même) quelque chose à modifier dans le code, mais qui est secondaire. Par exemple dans la fonction d’ajout de produit, j’ai inséré un commentaire TODO :

<?php 
    public function addProduct(string $sName, string $sCode, float $nPrice): void
    {
        /** TODO Mettre en indice du tableau le code produit 
        * cela sera plus facile à gérer et il y aura beaucoup moins de "for" 
        */
        array_push($this->aProduct,
            [
                'name'     => $sName,
                'code'     => $sCode,
                'price'    => $nPrice
            ]
        );
    }

Je vous présente aussi le « FIXME » qui est un niveau supérieur, reconnu lui aussi par votre IDE favori. Il permet d’alerter le dev sur un problème plus ou moins urgent à réparer. Par exemple avec la fonction de suppression d’un produit dans le panier :

<?php     
    public function deleteProduct($sCode): void
    {
        /** FIXME Générer une erreur ou gérer le cas où le produit n'est pas présent
        * dans le tableau
        */
        for ($i = 0; $i < count($this->aProduct); $i++)
        {
            if ($sCode == $this->aProduct[$i]['code'])
            {
                unset($this->aProduct[$i]);
            }
        }
    }

Et pour ceux qui aiment les documentations, vous pouvez aussi documenter les fonctions – si vous avez le temps, bien sûr. Il faut commencer par une petite description de la fonction, suivie des paramètres de la fonction et terminée par le retour de la fonction. Voici un petit exemple avec la fonction de récupération d’un produit :

<?php    
    /**
    * Récupérer un produit par rapport à un code.
    * @param string $sCode le code du produit en question
    * @return array un tableau décrivant le produit ou un null 
    */
    public function getProduct(string $sCode):?array
    {
        for ($i = 0; $i < count($this->aProduct); $i++)
        {
            if ($sCode == $this->aProduct[$i]['code'])
            {
                return $this->aProduct[$i];
            }
        }
        return null;
    }

On peut mettre plusieurs commentaires @param pour les paramètres mais il n’y aura toujours qu’un seul @return car on ne peut retourner qu’un seul objet. Par exemple :

<?php
    /**
    * Ajouter un produit dans le panier
    * @param string $sName le nom du produit
    * @param string $sCode le code du produit
    * @param float $nPrice le prix du produit
    * @return void
    */
    public function addProduct(string $sName, string $sCode, float $nPrice): void
    {
        /** TODO Mettre en indice du tableau le code produit 
        * cela sera plus facile à gérer et il y aura beaucoup moins de "for" 
        */
        array_push($this->aProduct,
            [
                'name'     => $sName,
                'code'     => $sCode,
                'price'    => $nPrice
            ]
        );
    }

Ma solution

La solution finale

Je pense avoir faire le tour sur le reformatage de code. Je vous propose une correction complète du code que je vous ai présenté plus haut :

Le fichier Manager.php :

<?php 
include_once "Basket.php"; 

$oBasket = new Basket(); 
$oBasket->addProduct("Raspberry Pi", "RPI", 37.90);
$oBasket->addProduct("Klim Domination", "KLIMDOM", 59.90);
$oBasket->addProduct("iPhone X", "IPHX", 1000);

$aProductFromBasket = $oBasket->getProductPrice("IPHX");

echo "Le prix de mon produit est ".$oBasket->getProductPrice("IPHX");

$oBasket->deleteProduct("IPHX");

$aProductFromBasket = $oBasket->getProductPrice("IPHX");

if(null != $oBasket->getProductPrice("IPHX")){
    echo "Le prix de mon produit qui est toujours vivant est ".$oBasket->getProductPrice("IPHX");
}
else{
    echo "Il n'y a pas de produit dont le nom est IPHX";
}

$nPrice = $oBasket->getPriceTotal();

echo $nPrice;

Le fichier Panier.php :

<?php 

class Basket { 
    
    protected $aProduct = []; 
    
    /**
    * Constructeur du panier
    */
    public function __construct()
    { 
        $this->aProduct = [];
    }

    /**
    * Récupérer l'ensemble des produits
    * @return array $this->aProduct un tableau de produit
    */
    public function getProducts():array
    {
        return $this->aProduct;
    }

    /**
    * Ajouter un produit dans le panier
    * @param string $sName le nom du produit
    * @param string $sCode le code du produit
    * @param float $nPrice le prix du produit
    * @return void
    */
    public function addProduct(string $sName, string $sCode, float $nPrice): void
    {
        /** TODO Mettre en indice du tableau le code produit 
        * cela sera plus facile à gérer et il y aura beaucoup moins de "for" 
        */
        array_push($this->aProduct,
            [
                'name'     => $sName,
                'code'     => $sCode,
                'price'    => $nPrice
            ]
        );
    }

    /**
    * Récupérer un produit par rapport à un code.
    * @param string $sCode le code du produit en question
    * @return array un tableau décrivant le produit ou un null 
    */
    public function getProduct(string $sCode):?array
    {
        for ($i = 0; $i < count($this->aProduct); $i++)
        {
            if ($sCode == $this->aProduct[$i]['code'])
            {
                return $this->aProduct[$i];
            }
        }
        return null;
    }

    /**
    * Récupération d'un prix par rapport à un code
    * @param string $sCode le code en question
    * @return float le prix du produit ou un null
    */
    public function getProductPrice(string $sCode):?float
    {
        for ($i = 0; $i < count($this->aProduct); $i++)
        {
            if ($sCode == $this->aProduct[$i]['code'])
            {
                return $this->aProduct[$i]['price'];
            }
        }
        return null;
    }

    /**
    * Supprimer un produit
    * @param string $sCode le code du produit à supprimer
    * @return void
    */
    public function deleteProduct(string $sCode): void
    {
        /** FIXME Générer une erreur ou gérer le cas où le produit n'est pas présent
        * dans le tableau
        */
        for ($i = 0; $i < count($this->aProduct); $i++)
        {
            if ($sCode == $this->aProduct[$i]['code'])
            {
                unset($this->aProduct[$i]);
            }
        }
    }

    /**
    * Récupérer le montant total du panier
    * @return float le montant du panier
    */
    public function getPriceTotal():float
    {
        $nPrice = 0;
        for ($i = 0; $i < count($this->aProduct); $i++)
        {
            $nPrice += $this->aProduct[$i]['price'];
        }
        return $nPrice;
    }
}

J’espère que ça vous a plu ! A très bientôt pour un prochain article qui vous permettra de faire la plupart des choses présentées ici en moins de 2 secondes, grâce à PHPStorm. En attendant, je vous conseille de consulter mes précédentes astuces de Dev Web.

Vous pourriez aussi aimer

2 commentaires

Philippe 6 mai 2018 - 13 h 16 min

Bonjour,
Très intéressant cette suite d’articles « Astuces de dev » 🙂
Mais j’attends la suite, notamment « dans le prochain article, je vous montrerai comment le faire en 2 secondes avec PHPStorm. »
Et si possible avec moins de gif animés pour ponctuer chaque paragraphe : je n’en vois pas l’intérêt, ça fatigue l’œil qui est sans arrêt attiré par le « mouvement » et donc ça distrait la lecture, la concentration (j’en ai besoin de cette concentration, car certains articles sont parfois un peu difficile à appréhender au premier abord).
Quoi qu’il en soit, merci pour toutes ces astuces.
Philippe

Répondre
Jérémy PASTOURET 18 mai 2018 - 12 h 09 min

Bonjour, j’apprécie de recevoir des commentaires constructifs comme le votre. J’ai temporairement mis de côté cette série d’articles pour développer d’autres thématiques. L’article que vous attendez sortira très prochainement.
Jérémy.
PS : les avis comme le vôtre me permettent d’orienter mes sujets au plus près des besoins de mes lecteurs. S’il y a des thèmes particuliers qui vous intéressent, n’hésitez pas à m’en faire part.

Répondre

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.