Qualité de code

Amélioration de la lisibilité du code

Deux citations introductives

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

— Martin Fowler

Programs must be written for people to read, and only incidentally for machines to execute.

— Harold Abelson

Plan

  • Introduction

  • Conventions de codage

  • Nommage de variables et de fonctions

  • Éviter des mauvaises interprétations

  • Esthétique

  • Commentaires

  • Simplification du flot de contrôle

  • Conclusion

Introduction

Définition générale de la qualité

Degré pour lequel un système, un composant ou un processus répond aux besoins spécifiés.

Différents points de vue

utilisateurs, développeurs, gestionnaires, etc.

Différents facteurs

robustesse, efficacité, portabilité, maintenabilité, etc.

La qualité est une question de point de vue

  • Ici, nous nous intéressons au point de vue des développeurs et à la maintenabilité du code.

    Critères de maintenabilité

    concision, cohérence, modularité, simplicité, documentation interne.

Principes

  • Plus le code est simple à lire, plus il est maintenable.

  • Le code doit réduire le temps de lecture et compréhension pour les autres développeurs.

Plan

  • Introduction

  • Conventions de codage

  • Nommage de variables et de fonctions

  • Éviter des mauvaises interprétations

  • Esthétique

  • Commentaires

  • Simplification du flot de contrôle

  • Conclusion

Conventions de codage

La plupart des langages n’ont pas de règles complexes pour le nommage de fonctions et variables :
  • N’importe quelle chaîne de caractères alphanumériques est un identifiant valide.

    • Pas universel : Java, OCaml, etc., imposent des règles sur la casse de la première lettre de l’identifiant

   

Cependant, tous les langages ont des règles d’écriture appelées « conventions de codage ».
  • Proposées par les créateurs du langage ou par la communauté de développeurs.

Conventions de codage (suite)

Parfois, plusieurs conventions sont disponibles pour un même langage :
  • Pour assurer la cohérence, chaque projet ne doit adopter qu’une seule convention.

  • Utiliser la même convention à l’intérieur d’un projet est plus important qu’adopter la « meilleure » convention.

En TypeScript

  • Les variables et les fonctions utilisent le camelCase : on utilise les majuscules pour séparer les noms composés :

Nom souhaité

camelCase

"nom de famille"

nomDeFamille

"a réussi l’examen"

aReussiLExamen

Conseillé :

let message : string = 'Hello world!'
let maxNumberOfWords = 15
function writeLine(line : string) {...}

Déconseillé :

let MESSAGE : string = 'Hello world!'
let max_number_of_words = 15
function WriteLine(line : string) {...}
  • En TypeScript, les Types utilisent le PascalCase : la première lettre de chaque mot est en majuscule :

Nom souhaitéPascalCase

"nom de famille"

NomDeFamille

"a réussi l’examen"

AReussiLExamen

Conseillé :

type LabelledValue {...}
type Point3D {...}
type GraduateStudent {...}

Déconseillé :

type Labelled_Value {...}
type POINT3D {...}
type graduatestudent {...}

Les constantes

Il peut être intéressant de mettre les noms des constantes globales en majuscules (Screaming Snake Case)

const ANNEE_FONDATION : number = 2022
console.log(`Nantes Université a été fondée en ${ANNEE_FONDATION}`);

Autres styles de nommage

Kebab Case
kebab-case-looks-like-this
max-parallel-events
Snake Case
snake_case_looks_like_this
another_variable_name
Screaming Snake Case
ALSO_KNOWN_AS_UPPER_CASE_EMBEDDED_UNDERSCORE
COMMONLY_REFERS_TO_CONSTANTS

Plan

  • Introduction

  • Conventions de codage

  • Nommage de variables et de fonctions

  • Éviter des mauvaises interprétations

  • Esthétique

  • Commentaires

  • Simplification du flot de contrôle

  • Conclusion

Nommage de variables et de fonctions

Une question de taille

Évitez les identifiants d’un seul caractère :
  • par exemple a, b, f.

La taille doit être proportionnelle à la portée :
  • Petite portée, noms courts.

  • Large portée, noms longs.

Une question de taille (suite)

Une variable globale
const maximumNumberOfActiveThreads : number = 8;

ou

const MAXIMUM_NUMBER_OF_ACTIVE_THREADS : number = 8;
Une variable dont la portée est restreinte
function sumArray(values : number[]): number {
    let sum: number;
    for (let each of list) {
        sum = sum + each;
    }
    return sum;
}

Une question de précision

binary tree
  • Les noms des identifiants doivent être précis.

    • par ex. une fonction qui calcule la taille d’un arbre :

function size(tree: Tree): number {
    //(...)
}
  • De quoi s’agit-il ? De la hauteur ? Du nombre d’éléments ? De l’empreinte mémoire ?

Une question de précision (suite)

binary tree

Alternatives :

function height(tree: Tree): number {}

function numberOfNodes(tree: Tree): number {}

function footprint(tree: Tree): number {}

Évitez les noms génériques

  • Les identifiants génériques tels que tmp, aux ou retval veulent souvent dire :

— Je ne sais pas comment appeler ma variable !

 

function euclideanNorm(values: number[]): number {
    let retval: number = 0;
    for(let i : number = 0; i  < values.length ; i+=1) {
        retval += values[i] * values[i]
    }
    return Math.sqrt(retval);
}

Évitez les noms génériques (suite)

  • Le code précédent est lisible, mais l’identifiant de la variable (retval) n’apporte aucune information.

  • Maintenant, supposons que le développeur ait remplacé par erreur * par + :

retval += values[i] + values[i]
sumOfSquares += values[i] + values[i]

Quelle erreur est la plus visible ?

Exception

Parfois tmp désigne vraiment une valeur temporaire :
    if(begin > end) {
        let tmp: number = end;
        end = begin;
        begin = tmp;
    }

Parenthèse : peut-on échanger la valeur de deux nombres sans passer par une variable temporaire ?

Oui :

a = a + b;
b = a - b;
a = a - b;

Itérateurs de boucle

  • Traditionnellement, les identifiants i, j et k désignent des itérateurs de boucle :

for(let i : number = 0; i < 10; i+=1) {
    for(let j : number = 0; j < 20; j+=1) {
        for(let k : number = 0; k < 40; k+=1) {
            // instructions
        }
    }
}

Valeurs avec unités

  • Parfois, ajouter l’unité de mesure simplifie la lisibilité.

  • par ex. la fonction wait(delay) :

function wait(delay: number){}
  • De quelle unité de temps s’agit-il ? Secondes ? Millisecondes ?

  • Alternative :

function wait(delayMilliseconds: number){}

Valeurs avec unités (suite)

Considérez par exemple :

let start: number = now();
treatmentThatTakesTime();
let elapsed = now() - start;
console.log("The execution took " + elapsed.toString() + "seconds")

Et :

let startMilliseconds: number = now();
treatmentThatTakesTime();
let elapsedMilliseconds = now() - startMilliseconds;
console.log("The execution took " +
    elapsedMilliseconds.toString() + "seconds")

Lequel des deux morceaux de code révèle plus facilement l’erreur ?

Autres exemples

IdentifiantIdentifiant enrichi

name

unicodeName

path

absoluteFilePath, relativeFilePath

password

encodedPassword, unencodedPassword

Plan

  • Introduction

  • Conventions de codage

  • Nommage de variables et de fonctions

  • Mauvaises interprétations

  • Esthétique

  • Commentaires

  • Simplification du flot de contrôle

  • Conclusion

Mauvaises interprétations

  • Certains identifiants sont source de confusion.

  • Par exemple :

results = filter(clients, "birthyear > 2000")
Qu’est-ce que l’on filtre exactement ?
  • Les clients nés après l’année 2000 ?

  • Ou le contraire ?

    • Si premier cas : select() est un meilleur nom.

    • Si deuxième cas, reject() ou exclude() ?

Limites

  • Utilisez min et max pour les limites

  • Supposez que vous souhaitiez limiter à 24 le nombre d’articles dans un panier et que vous déclariez la constante suivante :

const tooManyCartItems: number = 24;

Le problème ici c’est que l’on ne peut pas dire s’il s’agit une limite inclusive ou non. Est-ce que le panier peut avoir 24 articles ou non ?

Un meilleur identifiant :
const maxNumberOfCartItems: number = 24;
  • Ici, la limite est clairement inclusive.

Intervalles

  • Utilisez first et last, plutôt que start, stop, pour les intervalles inclusifs :

first last

Intervalles (suite)

  • Parfois, il est utile d’utiliser des intervalles inclusifs/exclusifs

  • Par exemple, ils est plus simple d’écrire :

todayEvents = eventsInRange("22/1/2018 00:00", "23/1/2018 00:00")

que

todayEvents = eventsInRange("22/1/2018 00:00", "22/1/2018 23:59")
  • Dans ces cas, utilisez begin et end pour les intervalles inclusifs/exclusifs :

begin end

Booléens

  • Traditionnellement, les identifiants booléens sont précédés par is, has ou need(s) pour éviter l’ambiguïté.

Par exemple :
let readPassword: boolean = true;
  • Est-ce que la variable dit que le mot de passe a été lu, ou qu’il doit être lu ?

De meilleurs choix :
let isAuthenticated : boolean = true;
let needPassword    : boolean = true;

Plan

  • Introduction

  • Conventions de codage

  • Nommage de variables et de fonctions

  • Éviter des mauvaises interprétations

  • Esthétique

  • Commentaires

  • Simplification du flot de contrôle

  • Conclusion

No matter how slow you are writing clean code, you will always be slower if you make a mess.

— Uncle Bob Martin

Esthétique

  • Un programme bien formaté est plus lisible qu’on programme mal formaté.

Exemple : PGCD (plus grand commun diviseur)
function pgcd(a: number, b: number): number
{
	while (a!=b){
	if (a>b){ a-=b ;}
	else {b-=a;}
	}
	return a;}

Esthétique (suite)

Exemple: calcul du PGCD
function pgcd(a: number, b: number): number
{
	while (a!=b){
	if (a>b){ a-=b ;}
	else {b-=a;}
	}
	return a;}
La même fonction bien formatée
// Calcul du plus grand commun diviseur de
// deux entiers
function pgcd(a: number, b: number): number {
    while (a != b) {
        if (a > b) {
            a = a - b;
        }
        else {
            b = b - a;
        }
    }
    return a;
}

Tabulations

Sans :

title = request.POST.get('title')
email = request.POST.get('email')
location = request.POST.get('location')
email = request.POST.get('email')
url = request.POST.get('url')
details = request.POST.get('details')

Avec :

title    = request.POST.get('title')
email    = request.POST.get('email')
location = request.POST.get('location')
email    = request.POST.get('email')
url      = request.POST.get('url')
details  = request.POST.get('details')

Laissez respirer le code

function validateEmail(email: Email): string {
 let error:string="";
 let tfl:string = email.value.trim();
 let emailFilter:RegExp = /^[^@]+@[^@.]+\.[^@]*\w\w$/ ;
 let illegalChars:RegExp = /[\(\)\<\>\,\;\:\\\"\[\]]/ ;
 if (email.value === "") {
     email.style.background = 'Yellow';
     error = "You didn't enter an email address.\n";
 } else if (!emailFilter.test(tfld)) {
     email.style.background = 'Yellow';
     error = "Please enter a valid email address.\n";
 } else if (email.value.match(illegalChars)) {
     email.style.background = 'Yellow';
     error = "The email address contains illegal characters.\n";
 } else {
     email.style.background = 'White';
 }
 return error;
}
  • Utilisez les lignes vides pour créer des strophes de code :

function validateEmail(email: Email): string {
    const emailFilter : RegExp = /^[^@]+@[^@.]+\.[^@]*\w\w$/;
    const illegalChars: RegExp = /[\(\)\<\>\,\;\:\\\"\[\]]/;

    let error   : string = "";
    let tfl     : string = email.value.trim();

    if (email.value === "") {
        email.style.background = 'Yellow';
        error = "You didn't enter an email address.\n";

    } else if (!emailFilter.test(tfld)) {
        email.style.background = 'Yellow';
        error = "Please enter a valid email address.\n";

    } else if (email.value.match(illegalChars)) {
        email.style.background = 'Yellow';
        error = "The email address contains illegal characters.\n";

    } else {
        email.style.background = 'White';
    }

    return error;
}

Indentez le code

Quelle version est plus lisible ?

Sans commentaires

function adultOrMinor(age: number): string {
if (age >= 18) {
return "adult";
} else {
if(age <= 3){
return "baby";
}
return "minor";
}
}

Avec commentaires

function adultOrMinor(age: number): string {
  if (age >= 18) {
    return "adult";
  } else {
    if(age <= 3){
      return "baby";
    }
    return "minor";
  }
}

Exemple d’erreur d’indentation

Version simplifiée d’un manuel du RER D

1er CAS : Aucun robinet d’arrêt n’est fermé :
    Purger la valve
2e CAS : Un robinet d’arrêt est fermé :
    Ouvrir le robinet
    Dans les 2 cas :
      - ouvrir le robinet d’arrêt
      - vérifier les freins du dernier véhicule
Remettre le train en marche

Si aucun robinet d’arrêt n’est fermé, le mécanicien doit-il vérifier les freins ?

Correction de l’indentation

Version corrigée dudit manuel du RER D

1er CAS : Aucun robinet d’arrêt n’est fermé :
    Purger la valve
2e CAS : Un robinet d’arrêt est fermé :
    Ouvrir le robinet
Dans les 2 cas :
    - ouvrir le robinet d’arrêt
    - vérifier les freins du dernier véhicule
Remettre le train en marche

Erreur d’indentation et conséquences dramatiques

L’erreur d’indentation de ce manuel ferroviaire a causé la mort de 56 personnes à Paris en 1988
Accident ferroviaire gare de Lyon 1988

Indentation sémantique

Certains langages imposent l’indentation comme faisant partie de la sémantique

Exemple

  • Python

annee = 2026
if annee == 2026:
print("Cette année")

…donne une erreur !

IndentationError: expected an indented block after 'if' statement on line 2

Plan

  • Introduction

  • Conventions de codage

  • Nommage de variables et de fonctions

  • Éviter des mauvaises interprétations

  • Esthétique

  • Commentaires

  • Simplification du flot de contrôle

  • Conclusion

Commentaires

  • Un code bien commenté est plus lisible qu’un code sans commentaires.

   

beaucoup commenter ≠ bien commenter !

   

  • Un code avec trop de commentaires inutiles est souvent plus difficile à lire qu’un code sans commentaires.

Un mauvais commentaire paraphrase le code

// Function declaration
function factorial(num: number): number {
    let factorial:number = 1; // Assigns 1 to factorial
    let i: number = num; // Assigns num to i

    while(i >= 1) {  // loops while i >= 1
        // assigns factorial with de product of factorial and i
        factorial = factorial * i;
        i--; // decrements i
     }
     return factorial; // return factorial
}
Tous les commentaires ci-dessus sont inutiles.

Quelques conseils

  • Ne commentez pas ce qui peut être déduit directement du code.

  • Ne commentez pas juste pour commenter.

  • Soyez précis !

  • Mettez-vous à la place de la personne qui lira votre code :

    • Anticipez les questions probables

    • Annoncez les pièges

    • Donnez une vision d’ensemble (résumé de l’algorithme)

Ne commentez pas les mauvais identifiants: renommez-les !

// Total number of active threads for handling requets
const threads: number = 5

Un changement du nom de l’identifiant peut remplacer un commentaire :

const numberOfActiveThreads = 5

Inclure les « commentaires du réalisateur »

  • Les « commentaires du réalisateur » sont des informations additionnelles dont seul le développeur dispose :

// I compared the performance several times using from 1 to 20 active
// threads and find out that 5 active threads achieves the best
// performance for 8 cores CPU.
const numberOfActiveThreads = 5;
/*
after hours of consulting the tome of google
i have discovered that by the will of unknown forces
without the below line, IE7 believes that 6px = 12px
*/
font-size: 0px;
  • Ce genre de commentaire peut aussi expliquer l’état du code :

// After adding new features for version 3.0 (November 2017), the
// code became fragile. Maybe we should split this function into
// two others, displayRaw() and displayHTML().
function display(){}

Commentez les failles du code

Table 1. Quelques commentaires spéciaux
CommentaireDescription

XXX

Mauvais code, mais qui fonctionne

FIXME

Mauvais code, qui ne fonctionne pas

TODO

À faire

HACK

Code réservé aux spécialistes

Commentaires internes et externes

  • Les commentaires externes à une fonction s’adressent aux utilisateurs.

  • Les commentaires internes s’adressent aux autres développeurs.

// Calculates the factorial of a number.
function factorial(num: number): number {
    let factorial:number = 1;
    let i: number = num;

    // A loop has better performance than recursive calls.
    while(i >= 1) {
        factorial = factorial * i;
        i--;
     }
     return factorial;
}

Plan

  • Introduction

  • Conventions de codage

  • Nommage de variables et de fonctions

  • Éviter des mauvaises interprétations

  • Esthétique

  • Commentaires

  • Simplification du flot de contrôle

  • Conclusion

Expressions booléennes

  • L’ordre de lecture d’une expression est importante :

if (length > 10)
  • est plus simple à lire que :

if (10 < length)

Opérateur ternaire

  • Comme d’autres langages, TypeScript propose un opérateur ternaire (opérateur conditionnel en une seule ligne).

  • Syntaxe : cond ? a : b (si condition alors a, sinon b)

  • Dans certains cas, le code est lisible et compact :

timeString = (hour > 12) ? 'pm' : 'am';
  • Mais dans la plupart des cas, le code devient plus complexe :

let isLeapYear: boolean = ((year % 400) === 0) ? true :
                            (((year % 100) === 0)? false :
                            (((year % 4) === 0)  ? true : false));

Opérateur ternaire : exemple

function adultOrMinor(age: number): string {
    if (age >= 18) {
        return "adult";
    } else {
        return "minor";
    }
}
function adultOrMinor(age: number): string {
    return (age >= 18) ? "adult" : "minor";
}

Variables explicatives

  • L’introduction de variables auxiliaires explicatives (ou constantes) permettent d’améliorer la lisibilité d’une expression complexe.

  • Ces variables sont souvent plus utiles que les commentaires.

  • Par exemple, considérez le code C++ suivant :

float _x = abs(x - deviceInfo->position.x) / scale;
int directionCode;
if (0 < _x && x != deviceInfo->position.x) {
  if (0 > x - deviceInfo->position.x) {
    directionCode = 0x04 /*left*/;
  } else if (0 < x - deviceInfo->position.x) {
    directionCode = 0x02 /*right*/;
  }
}
  • Le même code, sans commentaires, mais avec des variables explicatives :

static const int DIRECTION_CODE_RIGHT = 0x02;
static const int DIRECTION_CODE_LEFT = 0x04;
static const int DIRECTION_CODE_NONE = 0x00;

int oldX = deviceInfo->position.x;
int directionCode
  = (x > oldX)? DIRECTION_CODE_RIGHT
  : (x < oldX)? DIRECTION_CODE_LEFT
  : DIRECTION_CODE_NONE;

Retour anticipé d’erreurs

  • Souvent, il est recommandé de n’avoir qu’une instruction de retour dans une fonction.

  • Dans certains cas, comme dans la vérification des paramètres, le retour anticipé simplifie la lecture :

function contains(str: string , substr: string ): boolean {
    if (str === null || substr === null) return false;
    if (substr === "") return true;

    // (...)
}

Exemple, la fonction createUser()

function createUser(name: string, password: string, birthday: Date): User | undefined {
    let newUser: User | undefined = undefined;

    if (name !== undefined) {
        if (password !== undefined) {
            if (birthday !== undefined) {
                newUser = {
                    name: name, password: password, birthday: birthday
                };
            } else {
                console.log('User birthday is undefined');
            }
        } else {
            console.log('User password is undefined');
        }
    } else {
        console.log('User name is undefined');
    }
    return newUser;
}

La fonction createUser(), avec retour anticipé d’erreurs

function createUser(name: string, password: string, birthday: Date): User | undefined {
    if (name === undefined) {
        console.log('User name is undefined');
        return undefined;
    }
    if (password === undefined) {
        console.log('User password is undefined');
        return undefined;
    }
    if (birthday === undefined) {
        console.log('User birthday is undefined');
        return undefined;
    }

    return {name: name, password: password, birthday: birthday};
}

Expressions conditionnelles

  • Utilisez les expressions positives plutôt que les négatives.

  • Préférez :

if (a === b) {
    // Cas 1
} else {
    // Cas 2
}

à :

if (a !== b) {
    // Cas 2
} else {
    // Cas 1
}
  • Remplacez :

if (<booleanExpression>) {
    return true;
} else {
    return false;
}

Par :

return <booleanExpression>;
  • Par exemple, remplacez :

if (a > b) {
    return true;
} else {
    return false;
}

Par :

return (a > b);

Attention aux pièges (=)

Qu’affiche ce morceau de code ?

let annee = 2046;
if(annee = 2000){
    console.log("An 2000 !");
}else{
    console.log("Pas l'an 2000");
}
An 2000 !

Eh oui, confusion entre le = et le === (ou le ==) 👿

Erreur très classique dans les langages impératifs !

Attention aux pièges (==)

Qu’affiche ce morceau de code ?

if(2046 == "2046"){
    console.log("Test 1");
}
if(2046 === "2046"){
    console.log("Test 2");
}
Test 1

Explication : le == effectue un transtypage, pas le ===

Utiliser exclusivement le === en TypeScript.

Plan

  • Introduction

  • Conventions de codage

  • Nommage de variables et de fonctions

  • Éviter des mauvaises interprétations

  • Esthétique

  • Commentaires

  • Simplification du flot de contrôle

  • Conclusion

L’intention, la raison et la manière

  • Les commentaires externes doivent expliquer ce que fait une fonction (l’intention).

  • Les commentaires internes doivent expliquer pourquoi le code a été écrit comme ça (la raison).

  • Le code source doit être assez clair pour expliquer comment il s’exécute (la manière).

Conclusion

  • La lisibilité du code a un impact important sur la maintenabilité d’un logiciel.

  • La lisibilité ne concerne pas seulement la qualité des commentaires :

    • le choix des identifiants et la simplicité du flot de contrôle sont tout aussi importants.

Let us change our traditional attitude to the construction of programs: Instead of imagining that our main task is to instruct a computer what to do, let us concentrate rather on explaining to human beings what we want a computer to do.

Essay on Literate Programming
— Douglas Knuth

Final Quote

Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.

— John Woods