Introduction
Le code est le langage permettant de représenter les détails des exigences d’un modèle et d’une spécification. Quant à la programmation, elle est la spécification des exigences. Sa finalité étant d’être exécutée par une machine.
Si vous êtes développeur, ou que vous êtes passé par la case développeur, vous avez forcément déjà vu du mauvais code. On peut se retrouver à être ralenti dans notre productivité, même bloqué, à cause de son illisibilité, de sa complexité et de son ambiguïté.
À quel point une équipe peut-elle être ralenti par un mauvais code ? Imaginons qu’une équipe a pour projet la réalisation d’un logiciel de zéro. Au début de la phase de développement, leur productivité sera au maximum ; il n’y a aucun background, aucun existant qui pourrait porter atteinte à leur progression.
Cependant, plus l’équipe progresse dans le projet, plus des fonctionnalités sont créées, et les changements qu’elle apporte entraînent des répercussions sur d’autres parties du code. Au fur et à mesure, le bazar prend de l’ampleur et le code devient difficilement maintenable. Une simple modification prend des heures, plutôt que des petites minutes à cause du mauvais code. Un changement qui pouvait se résumer à la modification d’une simple ligne se retrouve à être celui de plus d’une centaine de lignes dans différents modules. Notre équipe se retrouve alors dépassée, avec une productivité s’approchant de zéro.
À quel point une entreprise peut-elle être impactée par un mauvais code ? Robert C. Martin dans son livre Clean Code (2008) énonce un exemple intéressant. Dans la fin des années 80, une entreprise a créé une application très populaire. Beaucoup de professionnels l’avaient acheté et l’utilisaient. Cependant, les cycles de mises à jour étaient de plus en plus longs, les bugs n’étaient pas corrigés au patch suivant. Les temps de chargement devenaient anormalement longs et les crashs devenaient récurrents. Peu de temps après, l’entreprise à fait faillite.
Robert C. Martin a pu interroger un ancien employé sur la situation, ce qui lui a confirmé ses craintes : le produit était « rushé » pour le déployer le plus vite possible sur le marché. Cette décision a provoqué un désordre dans le code. De plus, au fur et à mesure des ajouts de fonctionnalités, les bugs s’empilaient et le code s’empirait jusqu’à ce qu’ils ne puissent tout simplement plus le gérer.
« It was the bad code that brought the company down » – Robert C. Martin
Qu’est-ce que le Code Propre ?
Si vous avez en tête qu’un code désordonné est un réel obstacle, et que vous acceptez que le seul moyen d’être productif est de garder un code propre, vous avez déjà une longueur d’avance !
What is Clean Code ? Robert C. Martin a posé cette question à plusieurs programmeurs expérimentés afin d’avoir leur avis sur le sujet.
« I like my code to be elegant and efficient. The logic should be straightforward to make it hard for bugs to hide, the dependencies minimal to ease maintenance, error handling complete […], performance close to optimal […] » – Bjarne Stroustrup, inventor of C++
Bjarne Stroustrup utilise l’adjectif elegant. C’est un adjectif que l’on emploie que très rarement pour du simple « code ». Le message à retenir est qu’un code propre rend sa lecture plaisante, compréhensible, et que par conséquent, les bugs se dévoilent naturellement.
« Clean code can be read and enhanced by a developer other than its original author. It has unit and acceptance tests. […] It provides one way rather than many ways for doing one thing […] » – “Big” Dave Thomas
La citation ci-dessus appuie la précédente. Si un code est propre, tout le monde peut le comprendre et y apporter ses modifications simplement. De plus, “Big” Dave Thomas appuie sur l’importance de la présence de tests. En effet, un code sans test est difficilement vérifiable et donc mauvais.
En bref, un bon code :
- Ne contient surtout pas de duplication. J’insiste sur ce point car si à un moment, vous vous retrouvez à devoir copier-coller un bout de code, c’est qu’il y a un problème de design dans votre solution. Réfléchissez une fois de plus afin de transmettre plus clairement votre idée sur le code.
- Est simple et clair. Robert C. Martin ajoute qu’il doit se lire comme un journal, sans côté obscur et révéler pleinement l’idée derrière sa conception.
- Contient un minimum d’entité (classe, fonction, module, …).
- Est performant et optimisé.
- Valide tous les tests.
- Respecte les principes ci-dessous.
Quelques principes
Robert C. Martin compare le développement à un artisanat. On fabrique une solution correcte avec le fruit du travail acharné et de l’expérience. Attention, à la fin de cet article vous ne saurez pas subitement créer du code propre. Comme lire un article sur l’art ne fera pas de vous un artiste !
Nous allons parcourir quelques principes qui vous permettront d’approfondir vos connaissances sur le code propre avec des exemples.
Le code utilisé est le C#.
Le nommage
En mon sens cette partie est la plus importante ; toutes les entités d’un code ont un nom (fichier, classe, fonction, variable, …). Il est alors primordial de leur en donner un qui a du sens pour mieux se repérer dans le code, et surtout, pour mieux le comprendre !
Il est nécessaire d’utiliser des noms qui révèlent le rôle de l’entité. Le nom est censé répondre à 3 interrogations :
- La raison de l’existence de l’entité
- Ce qu’elle fait
- Comment elle peut être utilisée.
int f; // number of colleagues
Voici un exemple de mauvais nom ; il n’indique rien, ce n’est qu’une lettre sans aucun sens. Si vous décidez d’ajouter un commentaire à la déclaration de votre entité, c’est que le nom n’est pas assez clair. Il faut alors le changer !
int numberOfColleagues;
C’est déjà bien mieux. Le nom est assez clair pour comprendre l’objectif de notre variable. En plus, il n’y a plus de commentaire inutile.
Un autre exemple, imaginons que vous reprenez le code d’un de vos camarades sur un projet, et vous tombez sur cette fonction :
// Function that returns low items from a list of items
static List<int> GetLowItems(List<int> items)
{
List<int> list = new List<int>(); // List of items to return
foreach (int item in items)
{
if (item < 5) // 5 is 5g for 250ml
{
list.Add(item);
}
}
return list;
}
Rien de bien complexe dans cette fonction. Mais à quoi sert-elle ? Quel est son but ? À quoi correspond le chiffre 5 ? Le nom de la fonction GetLowItem n’est pas assez explicite. Les commentaires sont catastrophiques. Le choix de nom des variables n’arrange rien non plus ; list ? items ? item ? À l’aide !
static List<Drink> GetLowSugarDrinks(List<Drink> drinks)
{
List<Drink> lowSugarDrinks = new List<Drink>();
foreach (Drink drink in drinks)
{
if (drink.IsLowInSugar())
{
lowSugarDrinks.Add(drink);
}
}
return lowSugarDrinks;
}
Après avoir nettoyé toute cette histoire, on se retrouve devant un programme qui se lit clairement. Le choix des noms suffit amplement à comprendre le contexte et l’utilité de la fonction. Oui, depuis le début il s’agissait de boissons peu sucrées.
Les faux indices sont aussi des moyens efficaces de rendre un code incompréhensible et de commettre des erreurs.
Voici une liste de quelques principes pour nommer correctement vos entités :
- Éviter les mots contradictoires : ne pas nommer sa variable shoppingList si ce n’est pas une liste, ou encore déclarer une variable name au type int.
- Ne pas nommer deux variables avec une orthographe différente uniquement parce qu’ils ont le même objectif dans le même scope : nommer sa liste productz car products est déjà pris.
- Ne pas nommer ses variables v1, v2, … vn : ce n’est ni de la désinformation ni de la distinction inutile, c’est juste catastrophique car on ne peut rien en tirer.
- Bannir l’utilisation des mot « fourre-tout », exemple : comment pouvons-nous saisir la différence entre deux classes nommées ProductInfo et ProductData ?
- Ne pas utiliser le mot variable pour des variables, table pour une table, ainsi de suite…
- Utiliser des noms qui peuvent se prononcer à l’oral : msgtsndtusr. Comment ? Vous n’avez pas saisi qu’il s’agissait d’une variable contenant un message à envoyer à l’utilisateur ? On peut remplacer ce nom par messageToSendToUser.
- Nommer des variables qui se cherchent facilement :
- Les variables à une lettre sont à proscrire car elles sont impossibles à retrouver dans du code.
- Nommer sa variable i dans une boucle for pour l’itération n’est pas un problème, mais utiliser la lettre l peut causer des soucis car elle se confond avec le chiffre 1 !
- La longueur d’un nom doit correspondre à la taille de son scope. Plus le scope est grand, plus les noms que vous attribuez à vos entités doivent se démarquer.
- Les classes et les objets doivent posséder un nom ou un groupe nominal comme : Customer, User, AdressParser, PasswordEncrypter. Il faut éviter les mots comme Manager, Processors, Data et Info, car ils n’apportent aucune information importante. Une classe ne doit pas contenir un verbe dans son nom !
- Au contraire, les fonctions et les méthodes doivent contenir verbe dans leur nom : getName, sendMail, deleteProfile.
- Ne craignez pas d’employer des noms longs pour vos entités. Un nom long et descriptif est mieux qu’un nom court et énigmatique.
- Finalement, n’hésitez pas à renommer si vous trouvez mieux ! De nombreux éditeurs de code permettent de renommer des entités en quelques clics.
Les fonctions
Les fonctions sont des sous-routines contenant du code et qui exécutent une tâche spécifique. Elles prennent des paramètres en entrée et retournent un résultat. Les écrire correctement est le début de l’organisation d’un code.
Règle 1 : Une fonction doit être courte ! C’est la première règle à respecter. À partir de 20 lignes, nous pouvons considérer qu’une fonction est bien trop longue. Ayez pour objectif de toujours rester en dessous de 5-10 lignes ; c’est possible, et on en est tous capable (exemple vous pouvez diviser votre fonction en plusieurs sous-fonctions, …).
Règle 2 : La deuxième règle est tout aussi importante que la première : Une fonction doit réaliser une seule et une seule chose, et elle doit le faire parfaitement.
Pour faciliter les fonctions courtes, et qui ne font qu’une chose, le code d’une fonction doit se limiter à un même niveau d’abstraction. Si nous développons un Web Scraper (un programme informatique qui récupère les informations d’une page web pour effectuer des traitements), notre première fonction aura le niveau d’abstraction le plus élevé comme : GetHtml(url), et plus nous avancerons dans les sous-fonctions, plus l’abstraction sera faible, jusqu’au niveau le plus bas comme : Console.WriteLine(paragraph).
Règle 3 : The Stepdown Rule. Le code doit se lire de haut en bas, comme cet article. Le niveau d’abstraction diminue au fur et à mesure que vous parcourez la liste de fonctions. C’est une règle assez complexe à respecter, mais l’appliquer rendra votre code très agréable à lire.
Les arguments
- Le nombre d’argument idéal pour une fonction est de zéro (niladic).
- Un seul argument est correct (monadic), c’est assez simple à comprendre.
- Deux arguments est passable (dyadic), ça devient un peu plus complexe mais c’est justifiable.
- Trois arguments, ça commence à être problématique. Il faut les éviter (triadic).
- Plus de trois arguments, ce n’est tout simplement pas envisageable (polyadic).
L’une des astuces pour réduire le nombre de paramètres d’entrées d’une fonction est de créer des objets à partir de ses arguments ; si plusieurs arguments ont un concept qui les lie, il est alors possible de créer un objet à partir d’eux.
AddPoint(double x, double y); deviendrait AddPoint(Point point);
L’utilisation des DTO (Data Transfer Object) est aussi vraiment pratique pour considérablement réduire le nombre d’arguments d’une fonction.
Le combo verbe/nom donne un effet sympathique dans le nommage des fonctions. Draw(shape) est une bonne synergie, car on saisit tout de suite que la shape est Draw quelque part.
La gestion d’erreurs
Il est préférable d’utiliser des exceptions plutôt que des codes d’erreurs. L’utilisation de nombreux codes d’erreurs implique l’ajout de blocs if à rallonge. De plus, les blocs Try Catch peuvent être séparés de la logique d’une fonction et donc occupent moins de place dans le code.
Finalement, le traitement d’erreur est une « chose », donc si le mot-clé Try existe dans une fonction, il devrait être au tout début du code, et il ne devrait avoir aucun code après les blocs Catch et Finally.
Les commentaires
Les commentaires bien placés peuvent rendre l’analyse d’un code bien plus claire. Mais attention, il faut les utiliser à bon escient, car on peut facilement écrire un mauvais commentaire…
- Ajouter des commentaires à un pavé de code indique que vous n’avez pas pu vous exprimer correctement dans votre code, et qui est donc mauvais. Alors au lieu d’écrire un paragraphe explicatif, il faut améliorer votre code !
- Priorisez la clarté de votre code plutôt que l’utilisation de commentaires.
Les bons commentaires
Certains commentaires sont nécessaires :
- Les commentaires légaux : les copyrights, les licences, la mention des auteurs…
- Les TODO : mais attention ! L’utilisation de ce type de commentaire ne vous autorise pas d’écrire un mauvais code ou pire encore, un code qui ne fonctionne pas.
- Les clarifications : on peut parfois se retrouver devant un code très verbeux, sans pour autant pouvoir le modifier. Dans ce cas-là, il est possible d’ajouter des commentaires pour clarifier la situation.
- Des avertissements de conséquences : si un programme lance une tâche extrêmement longue, ou la suppression entière d’une base de données importante (imaginons), Il est alors important d’ajouter un commentaire pour amplifier l’information.
Les mauvais commentaires
D’autres commentaires sont immondes :
- Le marmonnage. Si vous devez écrire un commentaire nécessaire, exprimez-vous le plus clairement possible. Dans le rush on peut se retrouver à écrire une phrase incompréhensible.
- Les commentaires redondants. L’exemple ci-dessous est parfait. La signature de la fonction est assez claire, donc pourquoi ajouter un commentaire qui indique la même chose ?
// Function that returns low-sugar drinks from a list of drinks
static List<Drink> GetLowSugarDrinks(List<Drink> drinks)
{
// CODE
}
- Les commentaires trompeurs. Votre commentaire doit décrire EXACTEMENT ce que fait le code ciblé.
- La journalisation en commentaire est tout simplement inutile. GIT existe !
- Les commentaires après chaque }. Si votre bloc for ou votre fonction est si longue que vous devez mettre } //End of loop, } //End of function, essayez plutôt de réduire la longueur et la complexité de vos fonctions.
- Les codes commentés. Je pense que c’est le pire commentaire à faire. Pourquoi la fonction a été entièrement commentée ? Est-ce qu’elle était importante ? Est-ce qu’on peut la supprimer sans conséquence ? On ne peut tout simplement pas le savoir. Quoi qu’il en soit, supprimez vos fonctions obsolètes. Vous pourrez toujours les retrouver dans l’historique des commits sur GIT, et ça ne polluera pas le code actuel.
- Les commentaires avec trop d’informations. Soyez clair et concis !
Le formatage et l’indentation
Un code propre est un code suffisamment aéré. Le formatage est un concept très important pour maintenir un code lisible et clair.
Le formatage vertical
Dans son livre Clean Code (2008), Robert C. Martin traite de la métaphore du journal : Au début, on retrouve le titre qui amorce l’histoire qui sera racontée. Le premier paragraphe contient un synopsis qui balaye uniquement la surface, sans rentrer dans les détails. Plus on descend, plus les détails apparaissent. Pour un fichier source, nous sommes censés retrouver la même sensation.
- Chaque ligne d’un code représente une expression, chaque groupe de lignes représente un paragraphe, et chaque paragraphe doit être séparé par une ligne vide. Cette astuce permet de séparer les concepts entre eux, et ainsi garder une structure aérée.
- Si l’espace sépare les concepts, la proximité renforce l’association. Les lignes avec des concepts étroitement liés doivent paraître denses.
- Si une fonction en appelle une autre, elles doivent être verticalement proches dans le fichier source.
- L’ordre est important. Une fonction appelée doit être en dessous de la fonction appelante. Les concepts les plus abstraits apparaissent en premier et les plus précis en dernier (c’est le respect de cette règle qui permet de lire le code comme un journal).
- Attention, l’utilisation abusive de commentaires peut rompre une liaison proche !
Le formatage horizontal
Les lignes courtes sont la clé ! On se perd moins, et on force les lignes concises.
Le formatage horizontal se base sur l’utilisation pertinente de l’espace. Elle permet d’associer le code étroitement lié et dissocier le code qui l’est moins.
- Exemple pour une fonction :
- Aucun espace ne devrait apparaître entre le nom d’une fonction et la parenthèse ouvrante, car ils sont étroitement liés.
- On sépare d’un espace les arguments dans la signature d’une fonction pour mettre en valeur la virgule, et appuyer la séparation des arguments.
- Exemple pour les blocs if, for/while… :
- On ajoute un espace entre le mot-clé et la parenthèse ouvrante pour gagner en visibilité. De plus ce ne sont pas des fonctions, l’espace permet de les différencier.
- Un autre exemple pour une équation mathématique :
- Un nombre négatif n’aura pas d’espace entre le – et sa valeur (exemple -32).
- Cependant, une soustraction aura un espace avant et après le sigle – pour appuyer l’opération et séparer les valeurs (exemple return (-a – b);).
L’indentation
Un fichier source possède une hiérarchie. Pour qu’elle soit visible nous ajoutons une indentation pour chaque niveau de hiérarchie : la déclaration d’une classe a une indentation de 0, ses méthodes et ses attributs ont une indentation de 1, le code contenu dans un bloc a une indentation de plus que le bloc conteneur, ainsi de suite.
Chaque programmeur a sa manière de formater son code. C’est un sujet à débat ! Si vous travaillez dans une équipe, renseignez-vous et adoptez leur convention de formatage.
Une bonne pratique est aussi de se renseigner sur les conventions du langage que vous utilisez. Par exemple, on dit que Java et C# sont assez proches syntaxiquement, mais leur convention de nommage est bien différente !
L’utilisation d’un linter permet aussi de s’organiser sur la façon de rédiger du code collectivement.
Les classes
C’est le moment de s’attarder sur les niveaux d’organisations plus élevés : les classes. Il n’y a pas de code propre sans classes propres.
L’objectif est de maintenir le principe d’encapsulation.
Une classe doit être courte (oui aussi, comme les fonctions) ! Mais contrairement aux fonctions où on mesure leur taille par le nombre de lignes, on mesure une classe par le nombre de responsabilité.
On traite alors du respect du SRP (Single Responsibility Principle), l’un des principes SOLID :
Une classe ne doit avoir qu’une seule raison de changer, et donc doit avoir qu’une seule responsabilité.
Le SRP est l’un des principes les plus simples à respecter, pourtant on retrouve beaucoup de classes qui font plusieurs choses ! Pourquoi ?
- Nous avons tendance à prioriser un code qui fonctionne plutôt que l’organisation et la propreté.
- Les développeurs trouvent qu’une architecture avec plusieurs petites classes est plus complexe à comprendre qu’une architecture avec quelques classes volumineuses. Naviguer parmi tous ces fichiers peut s’avérer laborieux.
Cependant, il y a autant de code dans une architecture avec plusieurs petites classes qu’une architecture avec quelques grandes classes. Alors autant organiser nos classes pour mieux s’y retrouver !
- Si vous avez du mal à nommer une classe, c’est un indice qui montre que la classe a plus d’une responsabilité. Un autre indice est la présence de l’un des mots Processor ou Manager dans le nom de la classe.
- Une classe doit avoir un petit nombre d’attributs.
- Chaque méthode d’une classe devrait utiliser au moins un des attributs.
- Plus une méthode utilise les attributs de la classe, plus la classe a une forte cohésion.
- Maintenir la cohésion implique alors un grand nombre de petites classes
Conclusion
Nous avons balayé que la surface des principes du code propre, et c’est déjà pas mal. Mais maintenant, après avoir lu cet article, est-ce que vous pouvez dès maintenant écrire du code propre ? La réponse est malheureusement : non. Du moins, pas encore !
Le mot magique pour progresser sur la qualité de son code est la pratique. Si vous avez peu voire aucune connaissance dans le « Clean Code », lire la théorie ne vous transforme pas comme par magie en un développeur expert à la programmation irréprochable. Si un jour je me mets à regarder des vidéos sur des astuces de bricolage, je ne vais pas pouvoir rénover l’entièreté de mon appartement le lendemain.
L’essentiel est de pratiquer, se tromper, pratiquer une fois de plus, se corriger… Le développement est un artisanat qui se cultive, et on progresse en se trompant, en se corrigeant, et en étant attentif lors des revues de code.
D’ailleurs, il est très difficile de créer un code propre d’une seule traite. D’un point de vue personnel, lorsque je dois créer nouvelle fonctionnalité dans une application :
- Je commence par écrire un gros pâté moche, mais qui fonctionne et n’altère pas les autres fonctionnalités de l’application !
- Ensuite je commence à séparer les concepts de mon code dans différentes petites fonctions et des petites classes pour le rendre plus lisible et organisé. Bien évidemment, l’objectif est aussi de minimiser le plus possible les nombre d’entités.
- Je continue en supprimant entièrement la duplication. Finalement, je vérifie que mon code est compréhensible, que le nommage est suffisamment explicite, et surtout, je fais en sorte que mon code ne provoque pas de dommages collatéraux.
Maintenant c’est à vous ! Essayez de conclure votre journée en ayant laissé le code plus propre qu’il ne l’était à votre arrivée.
Source
Si vous voulez approfondir vos connaissances sur le sujet, je vous recommande vivement de lire le livre Clean Code (2008) de Robert C. Martin qui m’a permis d’appuyer mes propos sur cet article. C’est un incontournable pour toute personnes cherchant à s’améliorer dans le domaine de l’ingénierie logicielle.