Introduction
Pourquoi faire du clean code?
Le terme « clean code » désigne du code qui est facile à lire, à comprendre et à maintenir. Il suit probablement une liste de conventions et de standards qui rendent son cycle de vie plus aisé.
Avoir un code propre permet d’améliorer sa maintenabilité, son évolutivité et sa testabilité.
De plus, cela augmente la productivité de l’équipe, et réduit les risques d’erreurs.
Comment aider le développement à faire du clean code?
Peu importe votre poste, vous avez la possibilité d’inciter les développeurs à faire du clean code. En voici quelques pistes.
Refactoring : dans le cycle de vie d’une application, il est nécessaire d’entreprendre des phases de refactoring afin d’assurer la qualité du code et lutter contre la dette technique. Si ces phases sont faites régulièrement, elles en seront d’autant plus simples et rapides.
Revue de code : il est indispensable au sein d’un projet qu’il y ait un système de review (au travers d’une Pull Request typiquement). Cette relecture, en général par un Tech Lead, permettra d’obtenir des retours sur la qualité du code, son approche, et sa structure générale. Utilisez des outils complémentaires et adaptés à cet effet, tel que Sonar.
Testabilité : ne négligez pas les tests, ils font partie intégrante du clean code. Ils complètent la bonne compréhension du code, et garantissent le fonctionnement de l’application. Incitez à la rédaction de tests unitaires. Idéalement, privilégiez le TDD (Test Driven Development).
Pair-programming : faire du développement à deux permettra de partager les connaissances et les conventions, dont le clean code. Cela incite à la remise en question et à l’approche des implémentations.
Coding dojo : pourquoi se limiter à deux? Au sein d’un coding dojo, vous traiterez un exercice (kata) en groupe. Cela permet d’apprendre des autres et de partager un instant convivial.
Général
Gardez les choses simples (KISS)
Keep It Simple, Stupid. Réduisez la complexité de votre code au maximum.
Cela signifie que votre code doit être réalisé de la manière la plus simple possible. N’ajoutez pas de complexité artificielle. Cela facilitera sa maintenance.
Ne vous répétez pas (DRY)
Don’t repeat yourself. Bannissez la duplication. Elle entrainera une complexité de maintenance et de compréhension du code global. La duplication est source de bugs futurs : lors d’un prochain changement, vous devrez vérifier et avoir besoin de réaliser les changements aux différents endroits dupliqués.
Parfois, vous allez vouloir dupliquer une fonction et la modifier légèrement pour répondre à un besoin « presque semblable ». Cela peut relever d’une mauvaise structure, ou d’une mauvaise abstraction. Vérifiez le bon découpage de votre fonction. Repensez votre code avant d’entreprendre ce genre de démarche de duplication. Il est préférable d’appeler deux fois une fonction, que de dupliquer un bloc.
Règle du boy-scout : laissez le code plus propre que vous ne l’avez trouvé
Imaginez que vous n’ayez plus besoin de retaper et fixer le code de vos prédécesseurs. Ce serait possible seulement s’ils avaient appliqué cette règle ! Avant de modifier un code existant, rappelez-vous-en : laissez le code plus propre que vous ne l’avez trouvé.
De plus, cela permettra d’améliorer la qualité du code au fur et à mesure dans un cadre d’amélioration continue, et de lutter contre une future dette technique.
Appliquez le principe de moindre surprise
Derrière ce principe se cache l’idée d’éviter de surprendre l’utilisateur.
Typiquement, ce principe s’applique en UX : lorsqu’un utilisateur aperçoit l’icône d’une disquette, il s’attend à réaliser un enregistrement. S’il réalise le raccourci ALT+F4, il s’attend à fermer le programme en cours.
Ce principe s’applique également au code : une fonction doit implémenter un comportement auquel un programmeur peut s’attendre. Cela renforce la confiance et l’intuition de la personne lisant le code.
Le code doit être le plus simple et le plus facile à lire possible
Un code lisible est un code agréable. Il sera simple de le comprendre, et cela facilitera son évolution. Ne négligez jamais cet aspect si vous voulez éviter de vous torturer l’esprit.
Le code est écrit pour être lu par des humains !
Privilégiez un code court, clair, et simple. Cela est bien sûr vrai également pour les tests.
//bad code
int x = 10;
int y = 20;
int result = x + y;
System.out.println("Le résultat est : " + result);
//good code
int firstNumber = 10;
int secondNumber = 20;
int sum = addNumbers(firstNumber, secondNumber);
displayResult(sum);Suivez les conventions standards du langage, ainsi que de votre équipe
Chaque langage possède des conventions, que l’on peut retrouver au travers de sa documentation.
Si vous travaillez en équipe, il existe aussi probablement des conventions de codage que vous devez appliquer. À défaut, regardez le code existant, et suivez ses conventions. Si c’est du camelCase, faites de même, mais ne le mélangez pas avec du snake_case et autres conventions.
Cela évitera de complexifier la lecture du code, et sa compréhension globale.
//bad code int first_number = 10; int secondNumber = 20; int Sum = addNumbers(first_number, secondNumber); displayResult(Sum); //good code int firstNumber = 10; int secondNumber = 20; int sum = addNumbers(firstNumber, secondNumber); displayResult(sum);
Structure
Utilisez un formatage vertical
De manière générale, il est préférable d’aligner le code de façon verticale, et donc d’utiliser une indentation verticale.
Il est important d’espacer les différentes méthodes et concepts afin d’obtenir un code aéré.
Également, nous retrouvons généralement les méthodes les plus importantes en haut du fichier, et vers le bas les méthodes de bas niveau.
//bad code
private static void displayResult(int result) {
System.out.println("Le resultat est : " + result);
}
private static int addNumbers(int firstNumber, int secondNumber) {
return firstNumber + secondNumber;
}
public static void calcul() {
int firstNumber = 10;
int secondNumber = 20;
int sum = addNumbers(firstNumber, secondNumber);
displayResult(sum);
}
//good code
public static void calcul() {
int firstNumber = 10;
int secondNumber = 20;
int sum = addNumbers(firstNumber, secondNumber);
displayResult(sum);
}
private static int addNumbers(int firstNumber, int secondNumber) {
return firstNumber + secondNumber;
}
private static void displayResult(int result) {
System.out.println("Le resultat est : " + result);
}
Les variables et fonctions doivent être déclarées au plus près de leur utilisation
Cela facilitera grandement la lecture du code.
Une variable locale doit être déclarée juste au-dessus de sa première utilisation, et sa portée verticale doit être minimisée.
Aérez le code
Ne négligez pas le besoin d’aérer le code. Les différentes idées exprimées au travers du code doivent être espacées d’une ligne vide. Ces dernières sont des indices visuels qui ont un impact sur l’organisation d’un fichier.
// bad code
private static int addNumbers(int firstNumber, int secondNumber) {
return firstNumber + secondNumber;
}
private static void displayResult(int result) {
System.out.println("Le resultat est : " + result);
}
// good code
private static int addNumbers(int firstNumber, int secondNumber) {
return firstNumber + secondNumber;
}
private static void displayResult(int result) {
System.out.println("Le resultat est : " + result);
}
Utilisez les design patterns
Certes, les design patterns peuvent vous aider à résoudre différents problèmes de conception, mais également à améliorer la structure et la lisibilité de votre code. De plus, leur implémentation évite de réinventer la roue, et le lecteur pourra s’appuyer sur des concepts connus pour comprendre plus rapidement le code.
Séparez le code applicatif du code Framework
Cela offre une distinction entre la logique métier spécifique de l’application et les fonctionnalités générales fournies par le framework utilisé. Votre code sera d’autant plus modulable, lisible, testable, et réduit les dépendances.
Également, cela facilitera grandement toutes migrations vers une nouvelle version ou changement de technologies.
Ayez un seul langage par fichier source
Évitez de mélanger les différents langages au sein d’un même fichier. Cela le rendra plus modulaire, évitera certaines duplications de code, et améliorera sa lisibilité.
//bad code
Index.html
<div style="color: blue; font-size: 16px;" onclick="alert('Cliquez !')">Cliquez ici</div>
//good code
Index.html
…
<link rel="stylesheet" href="styles.css">
…
<div id="myDiv">Cliquez ici</div>
<script src="script.js"></script>
...
styles.css
#myDiv {
color: blue;
font-size: 16px;
cursor: pointer;
}
script.js
document.getElementById('myDiv').onclick = function() {
alert('Cliquez !');
};
Préférez le polymorphisme plutôt que les switch
Utilisez les switch à bon escient, mais n’en abusez pas. Rappelez-vous qu’une solution évidente n’est pas toujours la bonne solution. Ainsi, demandez-vous si une instruction switch ne pourrait pas être envisagée sous forme de polymorphisme.
// bad code
public class Shape {
public void draw(String type) {
switch(type) {
case "circle":
drawCircle();
break;
case "square":
drawSquare();
break;
default:
System.out.println("Type de forme non pris en charge");
}
}
private void drawCircle() {
System.out.println("Dessiner un cercle");
}
private void drawSquare() {
System.out.println("Dessiner un carré");
}
}
//good code
public abstract class Shape {
public abstract void draw();
}
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Dessiner un cercle");
}
}
public class Square extends Shape {
@Override
public void draw() {
System.out.println("Dessiner un carré");
}
}
Encapsulez les expressions conditionnelles
Lors de l’utilisation d’un if/else, encapsuler les expressions conditionnelles vous permettra d’avoir une meilleure lisibilité du code, et allégera vos méthodes.
//bad code if (id.hasExpired && !id.isRecurrent) //good code if (this.shouldBeDeleted(id))
Tests
Privilégiez le TDD
Le TDD (Test Driven Development) pourrait faire l’objet d’un sujet à lui seul.
De manière générale, anticipez et rédigez les tests avant le code. Cela apportera une meilleure construction applicative, et réduira les risques d’impacts et autres formes de régressions.
Un seul assert par test
Il est préférable de n’utiliser qu’un seul assert par test. Tout comme une méthode, un test doit se cantonner à une seule chose. Cela permet de réduire les dépendances entre les tests, et garantit qu’un test se concentre sur un seul concept, ce qui renforce également sa reproductibilité.
Gardez les tests aussi propres que le code
Ne négligez pas l’écriture des tests. Ils doivent suivre les mêmes principes de propreté que votre code. Ils participent activement à la compréhension du fonctionnement du code, et facilitent son évolutivité. Si vous ne prenez pas soin de vos tests, vous finirez par les passer lors de la compilation, et augmenterez le risque d’erreurs.
Utilisez un outil de coverage
Renforcez votre propreté de code avec un outil adéquat, tel que Sonar. Cela vous permettra non seulement de faire un suivi plus facile, mais également de prioriser votre refactoring. Dans la mesure du possible, vous vérifierez chaque modification de code avant de les fusionner sur la branche commune.
Les tests doivent être faciles à lancer
Les tests sont vitaux à toute étape d’intervention, de la compréhension du code à la garantie opérationnelle. S’il faut taper X lignes de commandes, voire faire de la configuration afin de lancer vos tests, allez-vous vraiment le faire à chaque fois que ce sera nécessaire, ou que vous en aurez l’envie ? Probablement que non.
Idéalement, il faut que vous puissiez lancer vos tests unitaires à l’aide d’une seule ligne de commande.
Chaque test doit être indépendant
Il est important que les tests ne dépendent pas les uns des autres. Vous devez pouvoir les exécuter comme bon vous semble, et dans l’ordre souhaité. Cela évitera un effet cascade, et renforcera le diagnostic global lors du passage des tests.
Appuyez-vous sur les mocks si nécessaire.
Nommage
Utilisez un nommage clair et descriptif
Toutes les fonctions, variables et autres entités que vous nommerez doivent refléter leur raison d’être, et de façon précise.
//bad code Int d ; //good code Int date ;
Utilisez un nommage prononçable et facile à rechercher
Si vous n’êtes pas capable de prononcer facilement à haute voix le nom de variable ou méthode, alors le nommage est sûrement à revoir. Pouvoir prononcer le nom sans ambiguïté est important pour la discussion au sein d’une équipe.
De la même manière, vous devez pouvoir facilement retrouver de mémoire un nommage : il n’est pas pratique de devoir retrouver le nom d’une variable pour la rechercher !
//bad code Int hhmmss; //good code Int date;
N’utilisez pas de préfixes, typages ou abréviations dans le nommage
Préciser le typage d’une variable au sein du code est inutile de nos jours. La plupart des IDE modernes apportent facilement ce genre d’informations. Si votre liste contient des utilisateurs, appelez-la users, et non arrayOfUsers, et encore moins arrUsers.
// bad code Array arrUsers; //good code Array users;
Remplacez les nombres magiques par des constantes
Les nombres magiques sont ces valeurs que l’on retrouve en dur dans le code, généralement en paramètre d’une méthode. Cela est vrai également pour les chaines de caractères.
Mettez-les toujours dans une constante, avec un nom significatif (et en majuscules). Cela rendra votre code moins obscur et plus lisible.
//bad code Navigation.setScreen(8); //good code Private static int GAME_OVER = 8; Navigation.setScreen(GAME_OVER);
Utilisez des noms pour les variables
User, Account, ou Picture sont des exemples de noms appropriés pour des variables. Ils sont parlants, et l’on sait à quoi s’attendre derrière ce terme.
Dans la mesure du possible, évitez les termes comme Data, Info, ainsi que les verbes.
// bad code Public Class UserInfo // good code Public Class User
Utilisez des verbes pour les méthodes et fonctions
getUser, deleteAccount, ou savePicture sont des exemples de verbes appropriés pour des méthodes et fonctions.
Vos méthodes et fonctions doivent décrire ce qu’elles font, et non la façon dont elles le font.
Si elles comportent un effet secondaire, celui-ci doit se refléter également dans le nom.
//bad code Private String text() //good code Private String getText()
Mettez les Booleans à l’affirmatif (is/has)
Il est plus facile de lire une condition affirmative qu’une condition négative. La plupart des langages possèdent le caractère « ! » pour exprimer l’inverse d’une condition. En cas d’utilisation d’une condition négative, vous pourriez alors vous retrouver avec une double négation.
//bad code Private boolean isNotEmpty; //good code Private boolean isEmpty;
Fonctions
Une fonction fait une seule et unique chose
Si votre fonction fait plus d’une chose, cela entrainera une forte complexité, aussi bien pour son écriture, que pour sa testabilité. Elle finira par se transformer en fonction fourre-tout, et perdra sa raison d’être.
Elles doivent être relativement courtes (< 20 lignes)
Une fonction plus courte sera plus facile à comprendre, aussi bien au sujet de sa raison d’être, que concernant son fonctionnement.
Idéalement, vos fonctions ne doivent pas dépasser 20 lignes. Et en aucun cas elles ne doivent dépasser les 100 lignes. Si c’est le cas, votre fonction ne respecte probablement pas les règles évoquées, et fait sûrement plus d’une chose.
Impliquant un niveau d’indentation < 3
Si votre niveau d’indentation est égal ou supérieur à 3, relisez votre fonction. Celle-ci comporte sûrement un niveau de complexité non souhaité, ou fait probablement plus d’une chose.
Elles possèdent, si possible, peu d’arguments (< 3)
Réduisez au maximum les arguments d’une fonction. Un trop grand nombre d’arguments ne fera qu’augmenter la complexité de votre code. Si c’est le cas, demandez-vous si votre méthode se trouve au bon endroit, ou s’il n’y a pas une class appropriée contenant déjà les informations nécessaires.
Évitez les Booleans en argument
Passer un Boolean en argument révèle souvent qu’une méthode fait plus d’une chose (une quand c’est true, et une autre quand c’est false).
Préférez la mise en place de deux méthodes distinctes.
Évitez le chainage de méthodes
Un objet doit éviter d’invoquer des méthodes d’un sous-objet lui appartenant.
C’est ce que l’on appelle la loi de Demeter : « Ne parlez qu’à vos amis immédiats ». Autrement dit, il faut éviter qu’un objet puisse accéder à la structure interne d’un autre objet, ou pire, de devoir l’utiliser et ainsi en être dépendant.
Cela rend l’objet difficilement maintenable et adaptable.
//bad code (A.getB().getC().getD();) //good code A.getSubItemOfB() ;
Commentaires
Ne soyez pas redondant
Voyons le code suivant :
i++; // incrément i
Nous pouvons voir que ce code n’apporte aucune plus value. Nous pouvons le considérer comme inutile.
Utilisez-les uniquement pour expliquer un code complexe ou non évident
Si un code nécessite d’être commenté, cela devrait vous mettre la puce à l’oreille : un problème plus profond pourrait s’y cacher. Le code est-il bien structuré ? Les variables sont-elles correctement nommées ? Attardez-vous sur les raisons d’ajouter un commentaire.
Parfois, cela reste nécessaire, afin d’apporter une explication sur un code dont la complexité est justifiable.
Pas de commentaire obsolète
N’oubliez pas que les commentaires doivent être maintenus, au même titre que le code.
Il est facile de négliger ce point lors d’un correctif ou d’une évolution, et de se retrouver avec un déphasage entre l’apport du commentaire, et le code qui lui est associé.
Pas de code en commentaire
Ne mettez jamais de code en commentaire, cela ne fait que polluer la lecture. Cela est d’autant plus inutile que vous pouvez retrouver le code historique dans votre gestionnaire de version (GIT, SVN).
De plus, un code commenté est destiné à prendre la poussière, et se retrouvera obsolète au fil des correctifs et évolutions de votre code.