Afin de changer un peu de mes articles purement techniques, j'ai décidé d'écrire sur des thèmes plus généraux, qui ne touchent pas uniquement le PHP ou symfony.
Pour cet article, j'ai décidé de me faire une réflexion sur un problème très récurrent dans le développement : la duplication et ses méfaits.
Dans notre travail de tous les jours nous sommes souvent confrontés à ce problème, soit directement, soit indirectement.
Les raisons de la duplication
La duplication de code peut être volontaire ou involontaire.
La duplication volontaire
Il y a forcement des moments dans la vie d'un développeur où l'on se sent obligé de dupliquer. On a l'impression que c'est la seule alternative possible. Ce qui est faux. Si vous tombez devant ce cas, c'est que vous avez fait de mauvais choix : l'architecture de votre composant n'est pas bonne. Une bonne façon d'éviter de rencontrer ce problème est de découpler au maximum vos composants. Voici un exemple :
Vous avez un programme qui doit exécuter consécutivement une tâche A et une tâche B complètement indépendantes. Vous codez alors une fonction qui fait AB sans découpler votre code. Mais alors, que va-t-il se passer si on vous demande de coder un nouveau comportement AC où C est une autre tâche indépendante de A ? Certains développeur vont avoir tendance à vouloir reprendre le code de la fonction AB pour créer votre fonction AC. Or, la, il y a duplication de la fonctionnalité A.
Par ailleurs, il y a des moments où les contraintes de temps sont très fortes sur un projet. Il faut "faire ça le plus rapidement possible". Devant cette contrainte, le développeur est tenté de reprendre du code d'un endroit pour le copier à un autre (l'éternel ctrl c ctrl v). Si au moment du développement on a l'impression d'avoir gagner du temps, lorsqu'il faut se replonger dedans pour débugger ou apporter des évolutions, on se rend vite compte de l'erreur : il faut repasser sur autant de code dupliqué.
Il y a même dans certains cas de la duplication obligatoire. Ce type de duplication est dépendant du langage notamment en C++ où dans un premier temps vous déclarer vos fonctions dans un fichier header, puis vous devez coder la fonction ensuite dans un autre fichier. Ce type de duplication est moins grave car en cas d'oubli de modification la déclaration, vous détecterez une erreur tout de suite car vous aurez une erreur de compilation.
La duplication involontaire
La duplication involontaire se produit suite à des erreurs d'origine humaine.
Il y a effectivement des projets sur lesquels beaucoup de développeurs travaillent en même temps. Si leur domaine d'application n'est pas clairement défini ou si ils travaillent sur des fonctionnalités liées, il y a des risques de créer de la duplication de code. Le meilleure moyen de lutter contre ce type de duplication est de favoriser la communication au sein de l'équipe de développement et de découper au maximum le travail des développeurs afin d'éviter qu'ils ne travaillent sur des .
La duplication peut aussi venir d'une erreur dans l'architecture de l'application. Par exemple, on retrouve ce type de duplication dans le cas d'un modèle de données où il y a des données calculées. J'ai récemment travaillé sur un projet de vente de séjours en ligne où nos produits avaient des disponibilités. Ces disponibilités étaient représentées par une table et on trouvait en colonne :
- vente_max : correspondant au stock de la disponibilité
- vente_tmp : correspondant aux ventes en cours
- vente_actuel : correspondant aux ventes de commandes validées
L'ajout de ces 3 colonnes était une bonne idée : elle permettait d'éviter de faire des requêtes en masse pour vérifier qu'une disponibilité était libre.
En SQL, pour récupérer les disponibilités libres, cela se traduisait par :
... WHERE d.vente_max > d.vente_actuel + d.vente_tmp
Mais cette tranquillité à un coût.
Les problèmes posés
La duplication pose de multiples problèmes. Tout d'abord, au niveau de la maintenance d'une application.
Dans le cas précédent, il faut s'assurer que les colonnes vente_actuel et vente_tmp sont correctement remplies sinon on risque d'avoir des incohérences sur la base de données. Pire, si des stocks sont pris mais jamais libérés en cas d'annulation, on peut arriver à une rupture de stocks qui entraine un maque à gagner. Afin de répondre à cette problématique, on doit mettre un écouteur sur les évènements de création et de suppression.Avec symfony, cela est très simple, il suffit de surcharger les méthodes save() et delete() des objets.
Je prend un autre exemple. Toujours sur ce même projet de vente de séjours en ligne, le client souhaitait ajouter un statut de visibilité sur ses produits (visible uniquement sur le front office, le back office, les deux ou complètement désactivé). Pas de problème, le champ statut existait déjà, il suffisait juste d'ajouter des valeurs dans un enum. je me met à la recherche du code du moteur de recherche.
Puis, je tombe sur le commentaire avec le texte suivant :
/* Si cette requête vient à changer, il faut également modifier la requête du coverflow et des incontournables */Le développeur ayant codé ceci ne travaille plus ce projet. Heureusement.
Bien évidemment, ce commentaire part d'un "bon sentiment" : le développeur voulait attirer l'attention sur la nécessité de modifier d'autres portions du code.
Toutefois, le développeur qui a ajouté ceci n'a pas poussé sa réflexion sur son code. Il a choisit la simplicité sur le moment.
Maintenant que j'ai récupéré ce projet et que je dois modifier cette requête, je dois le faire aux deux autres emplacements spécifiés dans le commentaire. Ce qui me fait perdre du temps.
De plus, est-ce que ce commentaire est à jour ? N y aurait-il pas une autre partie dans laquelle cette requête a été mise (ceci pause d'ailleurs le problème de duplication de la documentation et non seulement du code) ?
Je dois donc faire une recherche sur tout le projet sur un pattern dont je ne suis pas certain. Je perd encore du temps. Je trouve des résultats, effectivement la documentation n'était pas à jour. Il n y a rien de pire que de la documentation fausse ou incomplète.
Je fais les changements sur les autres requêtes et je perd encore du temps...
Au final, pour faire un changement simple, j'ai du modifier plusieurs parties du code du projet dont la plupart étaient totalement identiques. A total, j'ai peut-être passé 5 fois plus de temps que si l'information avait été à un et un seul endroit. De plus, si je n'ai pas modifié toutes les requêtes, on risque de passer à côté d'une régression, qui sans détection préalable risque d'être trouvée par le client. Ce n'est pas sérieux.
Des solutions ...
Un des bon principe de programmation est DRY : Don't Repeat Yourself. C'est-à-dire qu'une fonctionnalité ne doit exister qu'à un et un seul endroit.
Plus d'informations sur le concept DRY.
Vous devrez aussi parallèlement faire un travail de conception et de réflexion afin de segmenter votre code en un maximum de petits morceaux de codes. De cette manière vous allez grandement favoriser la réutilisation dans votre programme et vous éviterez les duplications.
De plus, lorsque l'on vous demandera de changer le comportement d'un composant, vous n'aurez qu'un seul et unique endroit où changer votre code. Vous mesurerez alors mieux les impacts d'une modification sur votre programme.
Afin de mieux illustrer mes propos, je vous propose un exemple très simple : vous devez mettre en place 4 fichiers PDF (création de commande, modification, annulation, confirmation) sur votre application. Chacun d'eux se compose d'un header commun à tous, d'un contenu spécifique à chaque PDF et d'un footer commun à tous. Quelle architecture allez-vous adopter pour votre programme ?
Pour ma part, je pense que le plus efficace est de créer :
- une classe pdfBase
Puis des classes héritant de pdfBase
- une classe pdfCreation
- une classe pdfUpdate
- une classe pdfCancel
- une classe pdfConfirm
Dans votre constructeur de pdfBase vous aurez :
$this->generateHeader();
$this->generateContent();
$this->generateFooter();
Vos méthodes generateHeader() et generateFooter() seront à placer dans votre classe pdfBase puisqu'elles sont communes.
La méthode generateContent sera à placer dans chacune des classes filles puisque le contenu sera spécifique. Vous pourrez même définir une méthode abstraite dans la classe pdfBase pour forcer vos classes filles à l'implémenter.
Ainsi, vous pouvez constater que cette architecture vous permet d'éviter la duplication. Et, lorsque votre client vous demandera de changer l'image contenue dans le header, vous n'aurez que le fichier pdfBase à modifier, ce qui est un gain de temps.
Pour réaliser cet article, je me suis basé sur ma propre expérience ainsi que le très bon libre "The pragmatic programmer" que je vous recommande vivement.