Passer au contenu principal

On parle beaucoup de « beau » code, maintenant pour savoir vraiment le définir il y a déjà moins de monde. Je donne ici quelques pistes, en sachant que le livre Clean Code est une référence sur le sujet. A savoir aussi qu’il n’existe pas une définition de « beau code », néanmoins un bon nombre de points font consensus.

Commençons par analyser un code source, ici du Java, en partant de l’exemple suivant :


public void applyDiscount(final Item item, final double discount) {
   if (discount >= 5 && discount <= 20.0) {
      double price = item.getPrice();
      price = price * (1.0 - (discount / 100.0));
      item.setPrice(price);
   } else {
      throw new IllegalArgumentException("Bad discount");
   }
}

La fonction est ici petite, mais elle pourrait être nettement plus grosse. Et rien que là il y a déjà plusieurs erreurs.

Si on part sur la ligne if (discount <=5 && discount >= 20) on remarque déjà les éléments suivants :

– Tout d’abord 5 et 20 sont des magic numbers. Il vaudrait mieux les remplacer par des constantes. Ceci s’applique pour à peu près toutes les valeurs hormis celles vraiment évidentes telles que 0, 1 et quelques autres.
– Deuxièmement, on risque de faire le test à chaque fois qu’on veut contrôler une réduction, autrement dit on obtient une duplication de code, beurk.
– Enfin la condition n’est pas très lisible. Ici ça va encore, mais si c’était plus compliqué ce serait franchement difficile à lire. Bref on a intérêt à la mettre dans une fonction séparée.
En appliquant ceci notre code devient :


private final static double MIN_VALID_DISCOUNT = 5;
private final static double MAX_VALID_DISCOUNT = 20;

private boolean isValidDiscount(final double discount) {
   return discount >= MIN_VALID_DISCOUNT
      && discount <= MAX_VALID_DISCOUNT;
}

public void applyDiscount(final Item item, final double discount) {
   if (isValidDiscount(discount)) {
      double price = item.getPrice();
      price = price * (1.0 - (discount / 100.0));
      item.setPrice(price);
   } else {
      throw new IllegalArgumentException("Bad discount");
   }
}

Dans le code ci-dessus on identifie clairement à la lecture qu’on veut vérifier la validité du paramètre discount. Bon d’accord là avec l’exception c’était évident mais ça pourrait s’appliquer sur des trucs un peu moins triviaux et là l’ensemble prendrait tout son sens.

La deuxième remarque qu’on pourrait faire à ce code est qu’on fait appliquer la réduction sur une instance d’Item qui contient le prix. Or finalement, en termes de rôle, il vaut mieux que ce soit cette instance en elle-même qui sache appliquer la réduction. En effet ça permet d’augmenter la réutilisabilité du code, ce qu’on cherche !

Dès lors on pourrait écrire dans la classe Item la méthode :


public class Item {

   private final static double PERCENTAGE_MAX = 100.0;

   private double price;

   // ...

   private boolean isDiscountInValidRange(final double discount) {
      return discount >= 0.0 && discount <= PERCENTAGE_MAX;
   }

   public void applyDiscount(final double discount) {
      if (!isDiscountInValidRange(discount)) {
         throw new IllegalArgumentException("Bad discount");
      }
      this.price = price * (1.0 - (discount / PERCENTAGE_MAX));
   }
}

Vous me direz : « mais pourquoi refait-on un contrôle sur isDiscountInValidRange ? ». La raison est en fait très simple : l’objet Item peut être utilisé n’importe où, y compris en dehors de notre méthode, or il est responsable de son état, autrement dit de la cohérence de ses données. C’est pourquoi par exemple il doit garantir que son prix ne va pas être négatif, ce qui serait le cas si la réduction était supérieure à 100 !

Une fois qu’on a ajouté cette méthode, on obtient pour la fonction d’origine le code suivant :


private final static double MIN_VALID_DISCOUNT = 5;
private final static double MAX_VALID_DISCOUNT = 20;

private boolean isValidDiscount(final double discount) {
   return discount >= MIN_VALID_DISCOUNT
      && discount <= MAX_VALID_DISCOUNT;
}

public void applyDiscount(final Item item, final double discount) {
   if (isValidDiscount(discount)) {
      item.applyDiscount(discount);
   } else {
      throw new IllegalArgumentException("Bad discount");
   }
}

Vous me direz qu’on aurait pu mettre tout ça dans la classe Item. Cette objection est parfaitement sensée, mais ça dépend en fait si on veut pouvoir appliquer différents jeux de règles métier sur la classe Item ou pas. Si on code directement dans Item l’intervalle de réductions valides, il sera plus compliqué d’intégrer celle-ci dans une librairie qu’on pourrait réutiliser dans des applications ayant des prérequis différents. D’un autre côté ça ferait aussi moins de code à écrire. Bref à ce niveau ça dépend des besoins, et rien d’autre.

Un « beau » code se doit d’être lisible

On trouve régulièrement dans la vraie vie des fonctions qui mesurent plusieurs centaines voire plusieurs milliers de lignes de long. Bien évidemment elles sont relativement illisibles, surtout quand vous commencez à utiliser des fonctionnalités avancées du langage telles que la réflexivité pour Java.

Autant dire que quand vous déboguez ça c’est un vrai supplice. J’ai ainsi eu le cas il y a quelques mois d’une fonction C de 3000 lignes dans laquelle il y avait un buffer overflow, sauf que le message du débogueur était tout sauf explicite et l’erreur semblait survenir à la fin de la fonction. Il nous a fallu plus d’une semaine pour trouver le coupable !!!

En d’autres termes faites de petites fonctions et méthodes ! Je ne vais pas donner de nombre de lignes maximum, ça dépend de la verbosité du langage, cf. la lecture de fichiers en Java… Mais ça n’empêche, vous rendrez comme ça votre code nettement plus lisible, regardez l’exemple ci-dessus. 😉

Un « beau » code doit être testable

Une bonne manière de savoir si votre code est beau est d’écrire des tests automatisés dessus. Si vous n’y arrivez pas car il y a bien trop de cas, c’est mauvais signes. De même si vous devez écrire des dizaines de tests sur une méthode, vous êtes clairement sur un code smell.

A noter que pour vous simplifier la tâche je recommande clairement pour le langage Java d’user et abuser des classes package private qui offrent le niveau de visibilité idéal pour ce genre de chose, à savoir des méthodes « privées » qui ne sont pas dans l’API mais qui peuvent être mockées facilement. L’équivalent n’existe malheureusement pas dans tous les langages, mais dans ce cas vous laissez vos classes en question publiques en les documentant clairement.

Le test driven development (TDD) : un bon moyen d’écrire du « beau » code

En utilisant le test driven development, vous garantissez la testabilité de votre code. Or comme tout bon développeur vous êtes sûrement flemmard, pas vrai ? 😉 Bref vous éviterez d’écrire de trop longs tests compliqués à maintenir, et par conséquent les méthodes écrites demeureront également simple.

Et si vous utilisez des constantes vous garantirez que si la valeur d’une des constantes change ça ne casse pas vos tests. Alors que si vous étiez passé par des magic numbers… Classe non ?

Un beau code doit être robuste

Les algorithmes ne sont rien d’autres que des fonctions mathématiques. Or ces dernières ont toutes un domaine d’applicabilité. Par exemple la fonction inverse ne s’applique pas sur la valeur zéro.

Il en va de même avec le code. Dès lors vérifiez toujours dans vos API que les paramètres entrés correspondent au domaine de validité de vos fonctions. Et comme ça peut parfois être compliqué il peut être utile d’écrire des fonctions de validation, appelées au début des implémentations de vos APIs.

Le nommage des fonctions et classes

Les fonctions et classes doivent décrire ce qu’elles font. Typiquement il vaut éviter ceci :


public void a(Object foo);
public void b(Object bar);
// ...

Au contraire ça pourra être :


public void filterList(final List<?> list, final Filter f);

L’idée est qu’en lisant votre code le développeur qui passera derrière vous doit pouvoir rapidement comprendre ce qu’il fait, ou du moins ce qu’il est supposé faire. Cette dernière information est extrêmement importante quand il faut corriger un bug.

Le cas de la documentation

Ce cas fait davantage débat, mais un point fait toutefois consensus : si vous livrez une API à des tiers celle-ci doit être impérativement documentée.

Vous devez notamment décrire vos fonctions, ainsi que leurs paramètres et leur domaine de validité. A noter que font partie de votre API toutes les méthodes et fonctions visibles de l’extérieur !. En Java ça va être tout ce qui est public et protected, en C tout ce qui est défini dans les .h ou qui n’est pas défini comme static dans les fichiers .c et ainsi de suite.

Personnellement en Java j’ai tendance à documenter jusqu’au niveau package private, voire dans certains cas au niveau private, mais à ce niveau là aucun problème. Par contre je vérifie que l’outil javadoc ne me génère aucun warning.

Un des bons points de la documentation est que justement elle vous force à comprendre ce que vous faites. Maintenant si vous devez écrire des commentaires pour expliquer vos algorithmes, ou pour indiquer des sections dans votre code, c’est qu’il est grand temps de redécouper celui-ci.

Les warnings du compilateur

Ca va sans dire mais mieux en le disant, à savoir qu’un code propre ne doit produire aucun warning à la compilation, sauf justification expresse qui apparaîtra en commentaire dans le code. Par exemple sous Debian ils ont corrigé un warning sur SSH qui n’aurait pas dû être fixé, mais avec un commentaire ce point aurait été évident.

De même utilisez tous les outils de validation de code à votre disposition tels que FindBugs et SonarQube pour Java, ou encore cppcheck et Valgrind pour C. Ceci permettra de grandement renforcer la confiance que vous avez dans votre code.

En bref…

Un « beau » code se doit évidemment d’être qualitatif (cf. les deux derniers points) mais également être très lisible et simple à maintenir. Pour ce faire il ne faut pas hésiter à le découper en petites fonctions ayant des noms explicites, ce qui permettra au passage d’éviter la duplication de code car dans le pire des cas vous pourrez facilement déplacer ces fonctions en cas de refactoring. De même vos classes devraient rester relativement petites. Par exemple au-delà de 2000 lignes il faut vraiment vous poser des questions…

Les API doivent être impérativement documentées, après pour le reste c’est plus sujet à débat.

Dernier point : faites attention au formatage. Les règles de typographie doivent être bien établies, et leur application doit être contrôlée par des outils. Les IDEs font généralement très bien ce travail, avec des outils de formatage. Après au niveau du gestionnaire de versions il ne vous reste qu’à vérifier que le fichier est correctement formaté avant d’accepter un commit.

 Cet article vous a plu ? Vous aimerez sûrement aussi :

Julien
Moi c’est Julien, ingénieur en informatique avec quelques années d’expérience. Je suis tombé dans la marmite étant petit, mon père avait acheté un Apple – avant même ma naissance (oui ça date !). Et maintenant je me passionne essentiellement pour tout ce qui est du monde Java et du système, les OS open source en particulier.

Au quotidien, je suis devops, bref je fais du dév, je discute avec les opérationnels, et je fais du conseil auprès des clients.

Son Twitter Son LinkedIn

Laisser un commentaire