Article écrit par Bastien
Qu’est ce que le clean code ?
En tant que dev, il vous est sans doute déjà arrivé de devoir développer une fonctionnalité, pourtant simple, mais de patauger à cause d’un code existant de mauvaise qualité.
Petite définition :
“Le clean code (code propre) est une approche de programmation qui se concentre sur l’écriture de code clair, lisible et facilement maintenable.”
Les raisons qui peuvent pousser à vouloir faire cela sont multiples.
- D’un point de vue de l’entreprise, la première motivation est financière. Un code clean est plus facile à prendre en main. On peut ainsi réduire drastiquement les temps passés à comprendre le code et à l’enrichir. Conséquence : sur le long terme on produit plus et mieux, pour une durée égale.
- D’un point de vue du développeur, on peut prendre du plaisir à adopter une démarche d’artisan logiciel “Software Craftsmanship”, et à créer des logiciels robustes. A contrario, maintenir un mauvais code peut être source de stress et de frustration.
Entrons dans le vif du sujet
Code “clair”, code “lisible”, code “maintenable”. D’accord. Mais qu’est-ce que ça signifie au juste ?
Dans la suite de cet article je vais essayer d’expliciter ces termes, qui peuvent sembler un peu flous au premier abord. Pour ce faire, je vais me baser sur des exemples (réels) de code que j’ai eu l’occasion de rencontrer.
Voici un extrait de code issu de la partie front-end d’une application javascript (framework AngularJS) sur laquelle il m’a été demandé de travailler :
// Methode pour creer le profil de l’utilisateur
$scope.create = function (idDemande) {
$log.debug('ModalInstanceGestionDemandesCtrl.create(' + idDemande + ')');
blockUI.start();
// GestionDemandesService.accepter(idDemande, $rootScope.currentUser.idProfil, 'CREATE', demandeAp).success(function() {
GestionDemandesService.accepter($scope.demande).then(function() {
GrowlService.manageSuccess(i18n.commons.articles.La + i18n.profil.demande + i18n.commons.messages.success\_create\_f);
}).catch(function() {
GrowlService.manageError(i18n.commons.messages.error\_create);
}).finally(function() {
blockUI.stop();
$modalInstance.dismiss('create');
if(!$scope.fromDemandeAp){
//Envoi vers la page de modification des habilitations du user.
$state.go('habilitation-edition',{id : idDemande});
} else {
//Récupération de l'entite par le profil
$state.go('demandes-ap-edition',
{
codeEntite : $scope.demande.codeEntite
});
}
});
};
Sans même être développeur AngularJS, certaines choses vous ont peut-être agacé.
Reprenons quelques points faciles à mettre en place et qui peuvent grandement vous aider à produire un code de meilleure qualité.
Commentaires
Les commentaires dans le code peuvent contribuer à faciliter la lecture et la compréhension du code. Attentions toutefois à ne pas tomber dans certains travers : il m’est arrivé de voir des blocs de dizaines de lignes commentés accompagnés de TODO vieux de plusieurs années, et qui ne seront probablement jamais dé commentés, ni même utilisés. Généralement, c’est du code à supprimer. Au pire, ce code sera toujours présent dans les méandres de votre système de versionning, et jamais totalement perdu.
Éviter les commentaires inutiles. Il m’est arrivé de tomber sur une classe Java dans laquelle chaque accesseur (getter et setter) était précédé d’un commentaire. De tels commentaires n’apportent rien si ce n’est des lignes superflues.
Dans mon extrait de code ci-dessus, j’ai ce commentaire, inutile et inchangé depuis 2019, je supprime :
// GestionDemandesService.accepter(idDemande, $rootScope.currentUser.idProfil, 'CREATE', demandeAp).success(function()
Quelques bonnes pratiques pour les commentaires :
- Commentez avec parcimonie, uniquement pour décrire les parties du code les plus complexes.
- Utilisez un style de commentaire uniforme.
- De façon générale, utilisez les commentaires pour décrire les motivations qui vous poussent à écrire une section de code. Commenter le “pourquoi ?” plutôt que le “comment ?”.
Naming des fonctions et variables
Parfois, en nommant intelligemment une fonction ou une variable, on se dispense de l’utilisation d’un commentaire. Dans le code ci-dessus, j’ai une méthode “create” (l.2) précédée d’un commentaire.
// Methode d'acceptation de la demande
$scope.create = function (idDemande) {
Ne serait-il pas plus judicieux de donner un nom plus explicite à la méthode, de façon à pouvoir se passer de commentaire ? Dorénavant au clic sur mon bouton, j’appelle la méthode :
$scope.handleCreateProfil = function() {
Avec un tel nom, “ce que fait la méthode” saute immédiatement aux yeux; ne pas hésiter à assembler plusieurs mots.
Quelques conseils de nommage :
- Lorsque c’est possible, utiliser la langue anglaise. Cela vaut pour le nommage de variable, de méthode (et plus généralement cela peut être une bonne pratique pour les messages de commit, les reviews git, les recherches techniques sur Google…)
- Respecter les conventions de casse propre à votre langage (CamelCase, lowerCamelCase, snake_case…), (thanks captain obvious).
- Utilisez des verbes pour décrire le comportement de méthodes, des substantifs/noms pour décrire des variables.
- Utilisez un vocabulaire précis, adapté en fonction de votre type de donnée. Si vous manipulez un booléen, précédez-le d’un préfixe “is”, “should”, “has”… Evitez par exemple le nom “flag”, trop vague, préferez quelque chose comme “isFlagValid”.
Single Responsibility Principle (ou SRP) et autres principes SOLID
En programmation orientée objet, le principe de responsabilité unique consiste à ne donner qu’une responsabilité à une classe ou à une méthode. En ne respectant pas ce principe, on peut se retrouver avec des classes ou des méthodes “fourre-tout” de centaines (ou milliers) de lignes.
Les conséquences peuvent être nombreuses :
- Complexité accrue, le code devient plus complexe à comprendre, à maintenir et à modifier.
- Fort couplage, le code est moins testable unitairement, on augmente les risques d’effets de bords imprévus.
- Code peu réutilisable
- Code difficilement maintenable
Le SRP est l’un des cinq principes SOLID promus par l’ingénieur logiciel américain Robert C. Martin. Difficile de concevoir un article sur le clean code, sans a minima les évoquer.
Dans mon code initial j’ai par exemple une clause finally qui implémente une fonction anonyme :
.finally(function() {
blockUI.stop();
$modalInstance.dismiss('create');
if(!$scope.fromDemandeAp){
//Envoi vers la page de modification des habilitations du user.
$state.go('habilitation-edition',{id : idDemande});
} else {
//Récupération de l'entite par le profil
$state.go('demandes-ap-edition',
{
codeEntite : $scope.demande.codeEntite
});
}
});
Je peux extraire cette implémentation dans une méthode séparée dismissModal et l’utiliser de la façon suivante :
.finally(() => dismissModal(closeModalLogMessage))
En procédant ainsi, on rend le code plus lisible, on enlève un niveau d’imbrication et la logique implémentée dans ma méthode dismissModal est réutilisable ailleurs. Dans le cas de mon application, j’ai plusieurs boutons, qui ont des comportements très proches. Je ne vais pas m’amuser à écrire quasiment le même code pour chaque bouton, je généralise autant que possible ce qui peut l’être ; ainsi mon code est factorisé et plus facilement maintenable.
J’ai détaillé ci-dessus le principe de SRP; les 4 autres principes SOLID sont :
- Le “Open/Closed Principle” : Il s’agit de faire en sorte d’avoir des entités (composants, classes, méthodes) fermés à la modification, mais ouverts à l’extension.
- Liskov Substitution Principle : Ce principe dit qu’un object de type A doit pouvoir être remplacé par un objet de type B si B est un sous type de A, et cela ne doit pas perturber la cohérence du programme.
- Interface Segregation Principle : Ce principe stipule qu’il vaut mieux diviser les interfaces utilisateurs en sous-interfaces plus petites et spécialisées. Les utilisateurs ne doivent pas être forcés d’interagir avec des méthodes ou des fonctionnalités qu’ils n’utilisent pas.
- Dependency Inversion Principle : Ce principe stipule que les modules de bas niveau ne doivent pas dépendre des modules de haut niveau. Au lieu de cela, les deux niveaux de modules doivent dépendre d’une abstraction commune.
Je conseille de lire un peu à ce sujet. Ces principes peuvent être un peu compliqués à assimiler (à titre personnel le principe d’inversion de dépendances me pose encore un problème). Si vous ne parvenez pas à les utiliser dans l’immédiat, c’est normal, mais petit-à-petit vous vous rendrez compte que cette façon de coder sera de plus en plus naturelle.
Norme de coding
Bien qu’elles soient propres à chaque langage, on peut s’accorder sur certaines conventions communes qui améliorent la lisibilité du code.
- On devrait éviter tant que possible les méthodes “trop longues”, qui sortent de l’écran et demandent de scroller. Par exemple : pas plus de 80 colonnes de largeur dans le code, 25 lignes max par fonction. Si votre code respecte bien les principes SOLID et que votre code est bien découpé, cela ne devrait pas être un problème.
- Variables définies dans un scope approprié, on n’utilise pas de variables globales si leur portée réelle ne concerne qu’une toute petite feature très locale.
- Ajout d’espaces avant et après les opérateurs binaires (=, +, -, /, %),
- Retours et sauts de lignes, respect des indentations…
- Pas trop de paramètres pour une méthode. Certains développeurs estiment qu’une méthode ne devrait pas avoir plus de 2 ou 3 paramètres.
Voici un aperçu de mon code à l’issue de ma refactorisation :
$scope.handleCreateProfil = function() {
let logMessage = i18n.commons.messages.logProfilCreation;
let successMessage = i18n.commons.messages.profilCreationSuccess;
let errorMessage = i18n.commons.messages.profilCreationFailure;
let closeModalLogMessage = i18n.commons.message.closeModalMessage;
prepareModal(logMessage);
GestionDemandesService.createProfil($scope.demande)
.then(() => displaySuccessMessage(successMessage))
.catch(() => displayErrorMessage(errorMessage))
.finally(() => dismissModal(closeModalLogMessage));
};
Plus lisible que le code d’origine, n’est-ce pas ?
Vous noterez quelques détails :
- Initialement les chaînes de caractères issues de l’internationalisation (i18n) étaient atomisées à un degré bien trop important. De façon générale attention à ne pas “sur-découper” le code; arrivé à un certain niveau, cela peut nuire à la qualité.
- Je n’ai plus de commentaires, et mon code est tout à fait lisible et compréhensible (du moins j’espère ^^)
- J’ai une moindre profondeur au niveau imbrications; fini les if/else sur 4 niveaux.
- Les implémentations de mes méthodes de callbacks “then”,”catch”, “finally” sont déléguées à d’autres méthodes que je réutilise ailleurs.
- La plupart des modifications que j’ai faites ne demandent pas de grosse compétence d’architecture logicielle. Mon exemple ne concerne qu’un seul fichier.
Pour finir
J’ai essayé de vous partager au travers de cet article quelques exemples de bonnes pratiques “simples” que j’essaye d’appliquer dès que possible pour produire du code plus facilement maintenable (et esthétiquement plus joli :D).
Parfois la réalité nous rappelle à l’ordre : délais à respecter, code existant inintelligible et sensible aux régressions, car peu testé unitairement, collaborations difficiles entre les personnes… Autant de choses qui peuvent freiner à faire les choses “bien”. On peut pester contre le code existant et les anciens développeurs, mais il ne faut pas oublier qu’eux aussi ont peut-être travaillé dans un contexte compliqué.
D’un point de vue du client, la refactorisation de code n’ajoute aucune valeur à un produit à court terme. Pour cette raison, elle est souvent non priorisée. En revanche, on peut (et on devrait) travailler cet aspect en tâche de fond lorsque l’on travaille sur le développement d’une features (renommer une variable, modifier un commentaire ne sont pas des tâches très chronophages).
Enfin, ne vous attendez pas à devenir expert du clean code en quelques jours, c’est un travail de long terme. Laissez-vous le temps d’apprendre, lire de la théorie, c’est bien, mais rien ne vaut de la pratique, beaucoup de pratique.
Pour progresser, attention à l’excès de confiance. (Et à l’inverse, gare au syndrome de l’imposteur !)
Quelques ressources pour aller plus loin…
- Voir principes KISS (Keep It Simple Stupid), [DRY (Don’t repeat yourself)](https://fr.wikipedia.org/wiki/Ne_vous_r%C3%A9p%C3%A9tez_pas#:~:text=Ne%20vous%20r%C3%A9p%C3%A9tez%20pas%20(don,les%20%C3%A9volutions%20de%20cette%20derni%C3%A8re.), Loi de Démeter
- Bon article sur le Dependency Inversion Principle (SOLID)
- Articles bonnes pratiques commentaires
- Article “How to Write Cleaner If Statements In JavaScript”
- Il existe des outils d’analyse de code, par exemple SonarQube, qui peut être integré à Jenkins. Voir article SonarQube sur le Medium Ouidou.
- Quelques bonnes pratiques de développement qui méritent de s’y intéresser : les tests unitaires (et le TDD, pour Test Driven Development), la revue de code, la documentation, les design pattern.
Quelques livres pour les plus motivés:
- Clean Code: A Handbook of Agile Software Craftsmanship (Robert Martin). Très bon ouvrage, souvent considéré comme la référence sur le sujet. Lecture accessible, même en anglais, il existe une traduction fr. Il y a des chapitres complets sur le naming de variables, les commentaires. En survolant cet ouvrage même quelques minutes, vous devriez apprendre un tas de choses utiles. Je recommande vraiment, quel que soit votre niveau en dev !
- Head First Design Patterns (Eric Freeman, Kathy Sierra). Je ne les ai pas évoqués dans cet article, mais les design pattern sont généralement des bonnes pratiques pour créer un code de qualité. Ils peuvent être un peu difficiles à appréhender et demandent un temps d’assimilation assez conséquent. Ce livre est bien illustré et se base sur des exemples de code intelligibles et très concrets.