Session de Comment Driven Refactoring

La théorie c'est bien, mais la pratique c'est mieux ! Je t'emmène dans une session de CDR.

Dans l'article sur le Comment Driven Refactoring, j'ai parlé des avantages du CDR et de pourquoi cette manière de refactorer fonctionne aussi bien.

Maintenant, on va passer à la pratique !

On va suivre pas-à-pas le déroulement d'un kata de Comment Driven Refactoring, de la mise en place du code jusqu'au résultat final.

Au fil de l'article, je te laisserais régulièrement l'occasion de chercher une réponse avant de te montrer le résultat que j'ai obtenu. Si tu le souhaites, tu peux voir ça comme un kata intéractif. N'hésite pas à suivre cet article avec ton IDE préféré ouvert sur le côté. 😉

L'exemple que j'utilise est en PHP version 7.4 (la meilleure ^^).

Tu es prêt ? C'est parti !

Mise en place

Avant de se lancer dans le CDR, il nous faudrait déjà un morceau de code à refactorer.

On va créer ce code.

Pour les besoins de la démo, on voudrait utiliser un code ni trop propre, ni trop sale. On veut plutôt du code représentatif de ce qu'on pourrait retrouver dans un logiciel quelconque.

Ma méthode pour créer ce genre de code est la suivante : je pars d'une base de quelques lignes (2 à 5 en général), et je la complète en improvisant.

La base

Voici le code de base qu'on va utiliser pour notre session :

$date ='1989-12-16';
$user = new User();
$user->setBirthDate($date);

Comme tu peux le voir, il ne s'y passe pas grand chose : on crée un utilisateur et on lui assigne une date de naissance.

Simple. Basique.

Les fioritures

Le seul problème avec ce code, c'est qu'il plante :

PHP Fatal error: Uncaught Error: Class "User" not found

Eh oui, c'est normal : la classe User n'existe pas !

On va donc la créer. On en profite aussi pour ajouter quelques règles métier supplémentaires.

Ne t'en fais pas si le code n'est pas très clair. C'est voulu. On arrange ça juste après. 😉

classUser
{
    protected$birthDate;

    public functionsetBirthDate($date)
    {
        $d=DateTime::createFromFormat('Y-m-d',$date);
        if (!$d) {
            throw newException('Bad date format');
        }
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
        $this->birthDate=$date;
    }
}

$date='1989-12-16';
$user= newUser();
$user->setBirthDate($date);

Pour résumer : quand on lui assigne une date de naissance, la classe User vérifie que la date est valide et qu'elle est bien dans le passé.

Ce sont des règles arbitraires, mais on va partir du principe qu'elles ont du sens pour notre exemple.

L'important, c'est que maintenant qu'on a ajouté la classe User, le script s'exécute correctement.

Le but de notre refacto est aussi un peu plus clair : on va améliorer la classe User.

On a maintenant tout ce qu'il faut pour se lancer, on peut y aller.

Étape 1 : ajouter des commentaires

Commençons notre session de Comment Driven Refactoring.

La première étape du CDR consiste à ajouter des commentaires dans le code pour l'expliciter.

La règle d'or de cette étape, c'est :

Le code final sera aussi bon que les commentaires qu'il contient.

Quand tu pratiques le Comment Driven Refactoring, il est normal de passer du temps sur cette étape : c'est là que tout se joue.

N'aie pas peur de choisir avec soin ce que diront les commentaires et veille à ce que les commentaires couvrent le maximum de code.

À ton tour

Quels commentaires tu ajouterais au code qu'on a pris comme exemple ? Quelles sont les clarifications que tu lui apporterais ?

N'hésite pas à te poser quelques secondes pour imaginer ce que ça donnerait. 👍

J'ai donc tenté d'apporter un maximum d'informations à la classe User sous forme de commentaires.

Voilà ce que ça donne :

/**
* A User
*/
classUser
{
    /**
     * @var string Date of birth format Y-m-d
     */
    protected$birthDate;

    /**
     * @param string $date Birth date
     * @return void
     * @throws Exception
     */
    public functionsetBirthDate($date)
    {
        // validate birth date
        $d=DateTime::createFromFormat('Y-m-d',$date);
        // assert is valid date
        if (!$d) {
            throw newException('Bad date format');
        }
        // assert date is in the past
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
        // store birth date
        $this->birthDate=$date;
    }
}

$date='1989-12-16';
$user= newUser();
$user->setBirthDate($date);

Là, c'est le moment où tu peux te poser un peu pour t'imprégner du code, car tout y est indiqué. Aucune autre information ne sera ajoutée ni retirée dans le code après cette étape.

C'est bon ? Tout est clair pour toi ?

Alors on a terminé la première étape !

Étape 2 : retirer des commentaires

Notre code regorge maintenant d'informations sous forme de commentaires. Transformons-les en code.

Ici, la règle du jeu, c'est de défoncer joyeusement supprimer les commentaires. 😈

C'est maintenant notre seul et unique but. Pas de pitié !

Règle d'or de l'étape 2 : on ne retire que ce qui est déjà clair dans le code.

Ça veut dire que les commentaires qui apportent des informations supplémentaires au code sont intouchables.

On va donc (souvent) devoir faire en sorte que le code change, afin que les commentaires deviennent redondants et qu'on puisse les supprimer.

Les commentaires inutiles

On va procéder pas à pas en augmentant progressivement le niveau de difficulté.

Tout d'abord, attaquons-nous au premier commentaire du code :

/**
* A User
*/
classUser

Ce commentaire nous indique que la classe User représente ... un User.

Non, sans blague ? Merci pour la précision ! 😁

Merci Captain Obvious !

En tout cas, c'est une bonne nouvelle pour nous.

On sait déjà que la classe User est un User : c'est littéralement écrit dessus.

Ce commentaire n'apporte pas d'information supplémentaire au code, donc on peut l'enlever.


/**
* A User
*/
classUser
{
    /**
     * @var string Date of birth format Y-m-d
     */
    protected$birthDate;

    /**
     * @param string $date Birth date
     * @return void
     * @throws Exception
     */
    public functionsetBirthDate($date)
    {
        // validate birth date
        $d=DateTime::createFromFormat('Y-m-d',$date);
        // assert is valid date
        if (!$d) {
            throw newException('Bad date format');
        }
        // assert date is in the past
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
        // store birth date
        $this->birthDate=$date;
    }
}

$date='1989-12-16';
$user= newUser();
$user->setBirthDate($date);

Et on n'a même pas eu à toucher au code en lui-même !

Ça, c'est fait.

On passe à la suite.

À ton tour

De la même manière que ce qu'on vient de faire, est-ce que tu vois un autre commentaire qu'on pourrait retirer sans rien changer d'autre ? Un commentaire qui explicite quelque chose qui est déjà évident.

Tu l'as trouvé ?

Voyons ça :


classUser
{
    /**
     * @var string Date of birth format Y-m-d
     */
    protected$birthDate;

    /**
     * @param string $date Birth date
     * @return void
     * @throws Exception
     */
    public functionsetBirthDate($date)
    {
        // validate birth date
        $d=DateTime::createFromFormat('Y-m-d',$date);
        // assert is valid date
        if (!$d) {
            throw newException('Bad date format');
        }
        // assert date is in the past
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
        // store birth date
        $this->birthDate=$date;
    }
}

$date='1989-12-16';
$user= newUser();
$user->setBirthDate($date);

En effet, la présence du birthDate = dans le code laisse peu de place à l'interprétation : on va stocker la date de naissance.

Le code nous dit même où on va le stocker (dans $this), ce que le commentaire ne fait pas.

Le commentaire store birth date est donc inutile : on peut le retirer.

Et un de plus en moins !

On passe au niveau supérieur ?

La PHPDoc

Intéressons-nous à la documentation.

À partir de maintenant, toutes les modifications qu'on devra apporter aux commentaires nécessiteront qu'on modifie aussi le code.

Est-ce que la PHPDoc compte comme un commentaire ? Est-ce qu'on doit aussi chercher à la supprimer ?

Oui. Techniquement, la PHPDoc c'est du commentaire, donc ça compte. Même si c'est un type spécial de commentaire.

On peut donc explorer cette voie.

Pour te donner un exemple, si on type le retour de la méthode setBirthDate, on n'aura plus besoin de préciser @return void dans la PHPDoc :


classUser
{
    /**
     * @var string Date of birth format Y-m-d
     */
    protected$birthDate;

    /**
     * @param string $date Birth date
     * @return void
     * @throws Exception
     */
    public functionsetBirthDate($date):void
    {
        // validate birth date
        $d=DateTime::createFromFormat('Y-m-d',$date);
        // assert is valid date
        if (!$d) {
            throw newException('Bad date format');
        }
        // assert date is in the past
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
        $this->birthDate=$date;
    }
}

$date='1989-12-16';
$user= newUser();
$user->setBirthDate($date);

Cela nous a permis de supprimer une ligne entière de commentaire !

À ton tour

Est-ce que tu vois une autre ligne de PHPDoc qu'on pourrait retirer en entier de cette manière ?

La réponse arrive juste après.


classUser
{
    /**
     * @var string Date of birth format Y-m-d
     */
    protected$birthDate;

    /**
     * @param string $date Birth date
     * @throws Exception
     */
    public functionsetBirthDate(string $date):void
    {
        // validate birth date
        $d=DateTime::createFromFormat('Y-m-d',$date);
        // assert is valid date
        if (!$d) {
            throw newException('Bad date format');
        }
        // assert date is in the past
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
        $this->birthDate=$date;
    }
}

$date='1989-12-16';
$user= newUser();
$user->setBirthDate($date);

Oui, c'était le paramètre de setBirthDate. Voyons pourquoi.

Que nous disait le commentaire ?

  • @param : "c'est un paramètre". Merci, mais on le savait déjà.
  • $date : son nom. C'est déjà écrit dans le code.
  • Birth date : ce qu'il représente (une date de naissance). Le code nous le disait déjà, vu que la méthode est un setter pour la date de naissance et que le paramètre s'appelle "date".

La seule info présente dans le commentaire mais pas dans le code, c'était string : le type du paramètre. Et maintenant qu'on l'a déclaré directement dans le code, on est bon là-dessus aussi.

En ajoutant le type du paramètre, le dernier élément de cette ligne de commentaire devient totalement redondant. Alors on peut supprimer toute la ligne.


Allez, on fait ça encore une fois ?

Cette fois, on va se concentrer sur le premier commentaire : string Date of birth format Y-m-d.

C'est l'occasion de préciser qu'on n'est pas obligé de retirer TOUT le commentaire d'un coup. Si le commentaire a toujours du sens au final, on peut juste enlever ce qui est redondant.

Voilà à quoi ça peut ressembler :


classUser
{
    /**
     * @var string Date of birth format Y-m-d
     */
    protectedstring $birthDate;

    /**
     * @throws Exception
     */
    public functionsetBirthDate(string $date):void
    {
        // validate birth date
        $d=DateTime::createFromFormat('Y-m-d',$date);
        // assert is valid date
        if (!$d) {
            throw newException('Bad date format');
        }
        // assert date is in the past
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
        $this->birthDate=$date;
    }
}

$date='1989-12-16';
$user= newUser();
$user->setBirthDate($date);

Ici, on a indiqué le type de $birthDate, ce qui nous a permis de retirer tout le commentaire, sauf ce qui parle du format de la date.

On finira par retirer ce qu'il en reste, tu verras. Mais pas maintenant. 😉

Laissons la PHPDoc de côté et augmentons encore de niveau.

L'intérieur de la méthode

L'intérieur de la méthode setBirthDate comporte trois commentaires. Pour le moment, héhé.

On va procéder dans l'ordre, et chercher à supprimer le commentaire validate birth date.

À ton tour

Comment ferais-tu pour que les informations présentes dans ce commentaire deviennent explicites dans le code ?


classUser
{
    /**
     * format Y-m-d
     */
    protectedstring $birthDate;

    /**
     * @throws Exception
     */
    public functionsetBirthDate(string $date):void
    {
        // validate birth date
        $d=DateTime::createFromFormat('Y-m-d',$date);
        // assert is valid date
        if (!$d) {
            throw newException('Bad date format');
        }
        // assert date is in the past
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
        $this->validateBirthDate($date);
        $this->birthDate=$date;
    }

    /**
     * @throws Exception
     */
    protected functionvalidateBirthDate(string $date):void
    {
        $d=DateTime::createFromFormat('Y-m-d',$date);
        // assert is valid date
        if (!$d) {
            throw newException('Bad date format');
        }
        // assert date is in the past
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
    }
}

$date='1989-12-16';
$user= newUser();
$user->setBirthDate($date);

On a extrait une méthode à partir du commentaire : validate birth date est devenu validateBirthDate().

Et à l'intérieur de cette méthode, on a mis toutes les lignes qui étaient concernées par le commentaire.

Résultat : le commentaire a disparu !

En plus, la méthode setBirthDate ne possède plus de commentaires. Ça veut dire qu'on est sur la bonne voie ! 💪

On passe au commentaire suivant :

$d=DateTime::createFromFormat('Y-m-d',$date);
// assert is valid date
if (!$d) {
    throw newException('Bad date format');
}

À ton tour

Comment tu ferais pour éliminer ce commentaire ?


classUser
{
    /**
     * format Y-m-d
     */
    protectedstring $birthDate;

    public functionsetBirthDate(string $date):void
    {
        $this->validateBirthDate($date);
        $this->birthDate=$date;
    }

    /**
     * @throws Exception
     */
    protected functionvalidateBirthDate(string $date):void
    {
        $d=DateTime::createFromFormat('Y-m-d',$date);
        // assert is valid date
        if (!$d) {
            throw newException('Bad date format');
        }
        $this->assertIsValidDate($d);
        // assert date is in the past
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
    }

    /**
     * @param DateTime|false $d
     * @throws Exception
     */
    protected functionassertIsValidDate($d):void
    {
        if (!$d) {
            throw newException('Bad date format');
        }
    }
}

$date='1989-12-16';
$user= newUser();
$user->setBirthDate($date);

Eh oui, on procède exactement comme à l'étape d'avant : le commentaire assert is valid date devient la méthode assertIsValidDate.

Note que son paramètre $d est soit un DateTime, soit égal à false, au cas où la date n'a pas pu être calculée correctement à la ligne d'avant (mauvais format ou autre). Pour ne pas perdre d'information, on a dû le préciser dans la PHPDoc de assertIsValidDate.

Hmmm ... cette refacto a supprimé une ligne de commentaires, mais elle en a créé deux autres.

T'es sûr de ton coup ?

Oui, t'inquiète. On s'en sort toujours avec le Comment Driven Refactoring.

L'important, c'est de supprimer au moins une partie d'un commentaire existant à chaque fois.

Si on crée d'autres commentaires au passage, c'est pas grave.

Tu verras qu'on finira par les supprimer aussi. 😉

À ton tour

Bon, ça fait deux fois qu'on transforme un commentaire en fonction, je suppose que tu sais déjà comment on va traiter le commentaire assert date is in the past.


classUser
{
    /**
     * format Y-m-d
     */
    protectedstring $birthDate;

    public functionsetBirthDate(string $date):void
    {
        $this->validateBirthDate($date);
        $this->birthDate=$date;
    }

    /**
     * @throws Exception
     */
    protected functionvalidateBirthDate(string $date):void
    {
        $d=DateTime::createFromFormat('Y-m-d',$date);
        $this->assertIsValidDate($d);
        // assert date is in the past
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
        $this->assertIsInThePast($d);
    }

    /**
     * @param DateTime|false $d
     * @throws Exception
     */
    protected functionassertIsValidDate($d):void
    {
        if (!$d) {
            throw newException('Bad date format');
        }
    }

    /**
     * @throws Exception
     */
    protected functionassertIsInThePast(DateTime $d):void
    {
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
    }
}

$date='1989-12-16';
$user= newUser();
$user->setBirthDate($date);

Le commentaire assert date is in the past a été changé en méthode assertIsInThePast.

Et pas besoin du mot "date" dans son nom, vu qu'on sait déjà qu'il prend une date en paramètre (un objet DateTime).

Le commentaire peut donc disparaitre.

Mais ... le code est encore plus gros que tout à l'heure !

C'était mieux avant !

T'as vu le bazar qu'on a mis dedans ? 😱

Peut-être, mais nous, on s'intéresse juste aux commentaires. Le code, c'est un détail pour le moment.

D'ailleurs, je vois même quelque chose d'encourageant à ce niveau : la méthode validateBirthdate ne possède plus de commentaires.

C'est très bon signe, on progresse. 💪

Dernière ligne droite

La meilleure partie, celle où la magie opère !

Jusqu'à présent, nos modifications n'ont pas amélioré notre code tant que ça.

La classe User a même beaucoup gagné en taille. Et je ne parle même pas du commentaire DateTime|false, qui me pique un peu les yeux !

Il est temps d'arranger ça. On passe aux choses sérieuses !

Schwarzy se prépare

Tu es prêt ?

Alors, on y va !

Tout à l'heure, on a laissé ce commentaire tout en haut du code :

/**
* format Y-m-d
*/
protectedstring $birthDate;

Je l'avais annoncé, il va passer à la casserole. Et ça va se faire maintenant !

À ton tour

Comment tu ferais pour éliminer ce commentaire ?


classUser
{
    /**
     * format Y-m-d
     */
    protectedstringDateTime $birthDate;

    public functionsetBirthDate(stringDateTime $date):void
    {
        $this->validateBirthDate($date);
        $this->birthDate=$date;
    }

    protected functionvalidateBirthDate(stringDateTime $date):void
    {
        $d=DateTime::createFromFormat('Y-m-d',$date);
        $this->assertIsValidDate($d$date);
        $this->assertIsInThePast($d$date);
    }

    /**
     * @param DateTime|false $d
     * @throws Exception
     */
    protected functionassertIsValidDate($d):void
    {
        if (!$d) {
            throw newException('Bad date format');
        }
    }

    /**
     * @throws Exception
     */
    protected functionassertIsInThePast(DateTime $d):void
    {
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
    }
}

$date='1989-12-16';
$d=DateTime::createFromFormat('Y-m-d',$date);
$user= newUser();
$user->setBirthDate($date$d);

Tu l'avais trouvé ? Si c'est le cas, félicitations ! 👍

L'objet DateTime en PHP n'a pas de format fixe pour la date. En transformant le string en DateTime, le commentaire n'a plus de raisons de rester là. Adieu !

Ça nous a aussi forcé à adapter setBirthDate et validateBirthDate. Maintenant, on instancie le DateTime avant de créer notre utilisateur.

D'ailleurs, à propos de ces modifications, tu ne vois pas quelque chose de bizarre dans le code ?

À ton tour

Tu as trouvé ce qui cloche suite à notre dernière modification ?

Eh oui, la méthode assertIsValidDate prend maintenant toujours un DateTime en entrée. Son paramètre ne sera donc jamais égal à false et la méthode ne lèvera jamais d'exception.


classUser
{
    protectedDateTime $birthDate;

    public functionsetBirthDate(DateTime $date):void
    {
        $this->validateBirthDate($date);
        $this->birthDate=$date;
    }

    protected functionvalidateBirthDate(DateTime $date):void
    {
        $this->assertIsValidDate($date);
        $this->assertIsInThePast($date);
    }

    /**
     * @param DateTime|false $d
     * @throws Exception
     */
    protected functionassertIsValidDate($d):void
    {
        if (!$d) {
            throw newException('Bad date format');
        }
    }

    /**
     * @throws Exception
     */
    protected functionassertIsInThePast(DateTime $d):void
    {
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
    }
}

$date='1989-12-16';
$d=DateTime::createFromFormat('Y-m-d',$date);
$user= newUser();
$user->setBirthDate($d);

La méthode n'a donc plus de raisons d'être. On peut la supprimer, ainsi que les deux commentaires qui lui sont rattachés.

Aparté : la gestion d'erreurs

Avec notre nouveau code, si on utilise une mauvaise date :

$date='not a valid date';
$d=DateTime::createFromFormat('Y-m-d',$date);
$user= newUser();
$user->setBirthDate($d);

On aura l'erreur suivante :

PHP Fatal error: Uncaught TypeError: User::setBirthDate(): Argument #1 ($date) must be of type DateTime, bool given

Attention : c'est un objet Error qui est généré dans ce cas, pas une Exception, comme c'était le cas avant.

L'erreur ne sera donc pas interceptée avec :

try {
    // le code
} catch (Exception $e) {
    // récupération de l'erreur
}

Mais les deux implémentent Throwable. On peut donc l'attrapper comme ça :

try {
    // le code
} catch (Throwable $t) {
    // récupération de l'erreur
}

Ici, on va partir du principe que le code gère correctement les Error, et donc que cette différence ne change rien au comportement du code.

Notre code ressemble maintenant à ça :

classUser
{
    protectedDateTime $birthDate;

    public functionsetBirthDate(DateTime $date):void
    {
        $this->validateBirthDate($date);
        $this->birthDate=$date;
    }

    protected functionvalidateBirthDate(DateTime $date):void
    {
        $this->assertIsInThePast($date);
    }

    /**
     * @throws Exception
     */
    protected functionassertIsInThePast(DateTime $d):void
    {
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
    }
}

$date='1989-12-16';
$d=DateTime::createFromFormat('Y-m-d',$date);
$user= newUser();
$user->setBirthDate($d);

Il ne reste plus qu'un seul commentaire dans la classe User.

Encore une dernière modification et on aura terminé !

À ton tour

Comment pourrait-on faire pour retirer le dernier commentaire de la classe User ?

Allez, découvrons la solution :


classBirthDateextendsDateTime
{
    public function__construct($datetime="now", ?DateTimeZone $timezone=null)
    {
        parent::__construct($datetime,$timezone);
        $this->assertIsInThePast();
    }

    /**
     * @throws Exception
     */
    protected functionassertIsInThePast():void
    {
        if ($this->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
    }
}

classUser
{
    protectedDateTimeBirthDate $birthDate;

    public functionsetBirthDate(DateTimeBirthDate $date):void
    {
        $this->validateBirthDate($date);
        $this->birthDate=$date;
    }

    protected functionvalidateBirthDate(DateTime $date):void
    {
        $this->assertIsInThePast($date);
    }

    /**
     * @throws Exception
     */
    protected functionassertIsInThePast(DateTime $d):void
    {
        if ($d->getTimestamp() >time()) {
            throw newException('Date must be in the past');
        }
    }
}

$date='1989-12-16';
$d=DateTimeBirthDate::createFromFormat('Y-m-d',$date);
$user= newUser();
$user->setBirthDate($d);

On a amélioré le DateTime qu'on utilisait jusqu'à présent en créant un objet qui effectue nativement les assertions dont on a besoin.

Voici comment ça s'est fait :

  1. On a étendu la classe DateTime pour créer BirthDate.
  2. On a déplacé le contenu de validateBirthDate dans le constructeur de BirthDate.
  3. On a déplacé la méthode assertIsInThePast dans la classe BirthDate.

En créant une classe qui représente une date qui est forcément dans le passé, l'assertion et le commentaire qui lui est attaché ont été déplacés dans la classe BirthDate.

Techniquement, on a retiré un commentaire de la classe User. Ça compte !

Et maintenant ? On fait quoi ?

Notre but était de retirer les commentaires de la classe User, et il n'y en a plus.

La refacto est donc terminée !

danse de la victoire

Si tu as tenu jusqu'ici, bien joué !

Comparaison finale

Prenons un peu d'altitude sur ce qu'on vient de faire, comparons le code avant et après refacto :


classUser
{
    protectedBirthDate $birthDate;

    public functionsetBirthDate(BirthDate $date):void
    {
        $d=DateTime::createFromFormat('Y-m-d',$date);
        if (!$d) {
            throw newException('Bad date format');
        }
        if ($d->getTimestamp() >$time) {
            throw newException('Date must be in the past');
        }
        $this->birthDate=$date;
    }
}

Alors, il est magnifique ou il est pas magnifique, ce code ? 😁

il est magnifique ou il est pas magnifique ?

Plus sérieusement, voici quelques-unes des choses qu'on peut remarquer :

  • Le code est plus lisible. Avant, il y avait 9 lignes possédant des informations importantes à la compréhension du code, et les informations étaient peu précises (pas de typage etc). Maintenant, la classe possède seulement 4 lignes contenant des informations importantes, et tout est précisément décrit.
  • Le code est plus simple. La classe User est réduite à une seule instruction ! Si on veut que le code fasse quelque chose, c'est le minimum possible. On n'aurait pas pu faire mieux que ça.
  • Le code est plus clair. On peut facilement résumer l'intégralité de la classe en quelques mots : "c'est un utilisateur à qui on peut assigner une date de naissance". Elle ne fait rien de plus et rien de moins.
  • Le code est plus robuste. La classe User ne lève plus d'exceptions et son comportement est prévisible à 100%.

En bref, le code s'est nettement amélioré.

On n'y a pas pensé !

Le Comment Driven Refactoring nous demande juste de penser aux commentaires. Faisons un petit tour d'horizon des choses auxquelles on n'a pas eu besoin de réfléchir.

  • Primitive Obsession. Notre code utilisait un string pour représenter une date de naissance, ce qui est un code smell. En convertissant le string en DateTime, puis en BirthDate, on a corrigé ce problème.
  • Fail Fast. Si le code doit planter, qu'il plante le plus tôt possible. Suite à nos modifications, le code ne peut plus lever d'exceptions/erreurs après la création du BirthDate. C'est propre !
  • Single Responsability Principle. User faisait trop de choses : d'une part il représentait un utilisateur, d'autre part il faisait de la gestion d'erreurs sur les dates. En créant BirthDate, on a permis à User de ne gérer que ce qui concerne un utilisateur et à BirthDate de ne s'occuper que de la validation des dates.
  • Single Level of Abstraction. Quand toutes les informations présentes dans un même bloc ont le même niveau d'abstraction, le code devient plus compréhensible. C'est ce qu'on a fait en extrayant les méthodes assert*.
  • Facade. Les commentaires "validate birth date", "assert is valid date" et "assert date is in the past" faisaient référence à ce qu'il se passait dans plusieurs lignes de code. En regroupant ces lignes dans un bloc (ici, une méthode) qui ne nous laisse voir que sa signature, on a appliqué le design pattern Facade.

Je pourrais continuer comme ça pendant longtemps.

En faisant une refacto classique, on aurait dû se prendre la tête réfléchir à ces choses pour aboutir à ce résultat.

Ici, on n'a pas eu besoin de s'encombrer le cerveau avec tous ces "détails" et avec ces grands principes pour progresser dans la refacto. Les grands principes sont venus d'eux-même dans le code.

On a juste cherché à retirer les commentaires du code, en suivant une seule règle simple ! Rien d'autre.

Conclusions

J'espère que ce kata t'a plu et que tu vois mieux comment fonctionne le CDR en pratique.

Comme promis, je suis entré dans les détails. Mais j'ai aussi survolé pas mal de sujets.

Contacte-moi si tu veux en discuter. 😉

Une dernière chose dont je n'ai pas parlé explicitement mais qui me paraît importante, c'est le côté ludique du CDR :

A chaque fois qu'on trouve un commentaire à ajouter, à améliorer ou à retirer, notre cerveau reçoit une petite dose de récompense chimique. C'est comme quand on fait du Test Driven Development et que les tests passent du rouge au vert.

Notre cerveau interprète ça comme une victoire. Et cette victoire, il l'obtient régulièrement et facilement. C'est parfait pour rester satisfait et motivé dans notre tâche.

Oui, le Comment Driven Refactoring, c'est fun !

← Retour au blog